underpost 3.2.30 → 3.2.80
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/ghpkg.ci.yml +87 -0
- package/.github/workflows/npmpkg.ci.yml +9 -6
- package/.github/workflows/publish.ci.yml +3 -3
- package/.github/workflows/pwa-microservices-template-page.cd.yml +0 -7
- package/.github/workflows/release.cd.yml +1 -1
- package/CHANGELOG.md +1230 -971
- package/CLI-HELP.md +14 -6
- package/README.md +3 -3
- package/bin/build.js +40 -4
- package/bin/deploy.js +2 -2
- package/bump.config.js +1 -0
- package/docker-compose.yml +26 -26
- 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 +15 -15
- package/scripts/disk-clean.sh +85 -60
- package/scripts/test-monitor.sh +1 -1
- package/src/api/core/core.controller.js +4 -65
- package/src/api/core/core.router.js +8 -14
- package/src/api/default/default.controller.js +2 -70
- package/src/api/default/default.router.js +7 -17
- package/src/api/document/document.controller.js +5 -77
- package/src/api/document/document.router.js +9 -13
- package/src/api/file/file.controller.js +9 -53
- package/src/api/file/file.router.js +14 -6
- package/src/api/test/test.controller.js +8 -53
- package/src/api/test/test.router.js +1 -4
- package/src/cli/cluster.js +104 -11
- package/src/cli/db.js +4 -2
- package/src/cli/deploy.js +60 -11
- package/src/cli/docker-compose.js +212 -1
- package/src/cli/fs.js +45 -25
- package/src/cli/image.js +147 -51
- package/src/cli/index.js +26 -6
- package/src/cli/release.js +50 -4
- package/src/cli/repository.js +98 -24
- package/src/cli/run.js +380 -178
- package/src/cli/secrets.js +75 -46
- package/src/cli/ssh.js +30 -11
- package/src/client/components/core/Modal.js +38 -4
- package/src/index.js +1 -1
- package/src/server/catalog.js +2 -0
- package/src/server/conf.js +163 -3
- package/src/server/downloader.js +3 -3
- package/src/server/middlewares.js +152 -0
package/src/cli/secrets.js
CHANGED
|
@@ -13,6 +13,58 @@ import { loggerFactory } from '../server/logger.js';
|
|
|
13
13
|
|
|
14
14
|
const logger = loggerFactory(import.meta);
|
|
15
15
|
|
|
16
|
+
// Shell/runtime-critical and Kubernetes-injected env keys that must never be persisted as
|
|
17
|
+
// application secrets nor injected into a pod via `envFrom`. An injected PATH (or HOME, etc.)
|
|
18
|
+
// overrides the container image's own and breaks coreutils/sudo resolution inside the pod
|
|
19
|
+
// ("rm: command not found"). Single source of truth for both container-env capture and the
|
|
20
|
+
// `underpost-config` secret built from an env file.
|
|
21
|
+
const RESERVED_ENV_KEYS = new Set([
|
|
22
|
+
'HOME',
|
|
23
|
+
'HOSTNAME',
|
|
24
|
+
'PATH',
|
|
25
|
+
'TERM',
|
|
26
|
+
'SHLVL',
|
|
27
|
+
'PWD',
|
|
28
|
+
'_',
|
|
29
|
+
'LANG',
|
|
30
|
+
'LANGUAGE',
|
|
31
|
+
'LC_ALL',
|
|
32
|
+
'container',
|
|
33
|
+
'SHELL',
|
|
34
|
+
'USER',
|
|
35
|
+
'LOGNAME',
|
|
36
|
+
'MAIL',
|
|
37
|
+
'OLDPWD',
|
|
38
|
+
'LESSOPEN',
|
|
39
|
+
'LESSCLOSE',
|
|
40
|
+
'LS_COLORS',
|
|
41
|
+
'DISPLAY',
|
|
42
|
+
'COLORTERM',
|
|
43
|
+
'EDITOR',
|
|
44
|
+
'VISUAL',
|
|
45
|
+
'TERM_PROGRAM',
|
|
46
|
+
'TERM_PROGRAM_VERSION',
|
|
47
|
+
'SSH_AUTH_SOCK',
|
|
48
|
+
'SSH_CLIENT',
|
|
49
|
+
'SSH_CONNECTION',
|
|
50
|
+
'SSH_TTY',
|
|
51
|
+
'XDG_SESSION_ID',
|
|
52
|
+
'XDG_RUNTIME_DIR',
|
|
53
|
+
'XDG_DATA_DIRS',
|
|
54
|
+
'XDG_CONFIG_DIRS',
|
|
55
|
+
'DBUS_SESSION_BUS_ADDRESS',
|
|
56
|
+
'GPG_AGENT_INFO',
|
|
57
|
+
'WINDOWID',
|
|
58
|
+
'DESKTOP_SESSION',
|
|
59
|
+
'SESSION_MANAGER',
|
|
60
|
+
'XAUTHORITY',
|
|
61
|
+
'WAYLAND_DISPLAY',
|
|
62
|
+
'which_declare',
|
|
63
|
+
]);
|
|
64
|
+
const RESERVED_ENV_KEY_PREFIXES = ['KUBERNETES_', 'npm_', 'NODE_'];
|
|
65
|
+
const isReservedEnvKey = (key) =>
|
|
66
|
+
RESERVED_ENV_KEYS.has(key) || RESERVED_ENV_KEY_PREFIXES.some((prefix) => key.startsWith(prefix));
|
|
67
|
+
|
|
16
68
|
/**
|
|
17
69
|
* @class UnderpostSecret
|
|
18
70
|
* @description Manages the secrets of the application.
|
|
@@ -49,58 +101,35 @@ class UnderpostSecret {
|
|
|
49
101
|
*/
|
|
50
102
|
createFromContainerEnv() {
|
|
51
103
|
Underpost.env.clean();
|
|
52
|
-
const systemKeys = new Set([
|
|
53
|
-
'HOME',
|
|
54
|
-
'HOSTNAME',
|
|
55
|
-
'PATH',
|
|
56
|
-
'TERM',
|
|
57
|
-
'SHLVL',
|
|
58
|
-
'PWD',
|
|
59
|
-
'_',
|
|
60
|
-
'LANG',
|
|
61
|
-
'LANGUAGE',
|
|
62
|
-
'LC_ALL',
|
|
63
|
-
'container',
|
|
64
|
-
'SHELL',
|
|
65
|
-
'USER',
|
|
66
|
-
'LOGNAME',
|
|
67
|
-
'MAIL',
|
|
68
|
-
'OLDPWD',
|
|
69
|
-
'LESSOPEN',
|
|
70
|
-
'LESSCLOSE',
|
|
71
|
-
'LS_COLORS',
|
|
72
|
-
'DISPLAY',
|
|
73
|
-
'COLORTERM',
|
|
74
|
-
'EDITOR',
|
|
75
|
-
'VISUAL',
|
|
76
|
-
'TERM_PROGRAM',
|
|
77
|
-
'TERM_PROGRAM_VERSION',
|
|
78
|
-
'SSH_AUTH_SOCK',
|
|
79
|
-
'SSH_CLIENT',
|
|
80
|
-
'SSH_CONNECTION',
|
|
81
|
-
'SSH_TTY',
|
|
82
|
-
'XDG_SESSION_ID',
|
|
83
|
-
'XDG_RUNTIME_DIR',
|
|
84
|
-
'XDG_DATA_DIRS',
|
|
85
|
-
'XDG_CONFIG_DIRS',
|
|
86
|
-
'DBUS_SESSION_BUS_ADDRESS',
|
|
87
|
-
'GPG_AGENT_INFO',
|
|
88
|
-
'WINDOWID',
|
|
89
|
-
'DESKTOP_SESSION',
|
|
90
|
-
'SESSION_MANAGER',
|
|
91
|
-
'XAUTHORITY',
|
|
92
|
-
'WAYLAND_DISPLAY',
|
|
93
|
-
'which_declare',
|
|
94
|
-
]);
|
|
95
|
-
const systemKeyPrefixes = ['KUBERNETES_', 'npm_', 'NODE_'];
|
|
96
104
|
for (const [key, value] of Object.entries(process.env)) {
|
|
97
|
-
if (
|
|
98
|
-
if (systemKeyPrefixes.some((prefix) => key.startsWith(prefix))) continue;
|
|
105
|
+
if (isReservedEnvKey(key)) continue;
|
|
99
106
|
Underpost.env.set(key, value);
|
|
100
107
|
}
|
|
101
108
|
},
|
|
102
109
|
},
|
|
103
110
|
|
|
111
|
+
/**
|
|
112
|
+
* @method sanitizeSecretEnvFile
|
|
113
|
+
* @description Strips shell/runtime-critical and Kubernetes-injected keys (PATH, HOME, …) from
|
|
114
|
+
* raw `.env` file content so the resulting `underpost-config` secret can be safely injected via
|
|
115
|
+
* `envFrom` without clobbering the container image's own PATH. Blank lines and comments are
|
|
116
|
+
* preserved. Uses the same {@link RESERVED_ENV_KEYS} blocklist as container-env capture.
|
|
117
|
+
* @param {string} envFileContent - Raw contents of a `.env.<env>` file.
|
|
118
|
+
* @returns {string} Filtered env-file content.
|
|
119
|
+
* @memberof UnderpostSecret
|
|
120
|
+
*/
|
|
121
|
+
sanitizeSecretEnvFile(envFileContent) {
|
|
122
|
+
return envFileContent
|
|
123
|
+
.split('\n')
|
|
124
|
+
.filter((line) => {
|
|
125
|
+
const trimmed = line.trimStart();
|
|
126
|
+
if (!trimmed || trimmed.startsWith('#')) return true;
|
|
127
|
+
const key = line.slice(0, line.indexOf('=')).trim();
|
|
128
|
+
return !key || !isReservedEnvKey(key);
|
|
129
|
+
})
|
|
130
|
+
.join('\n');
|
|
131
|
+
},
|
|
132
|
+
|
|
104
133
|
/**
|
|
105
134
|
* Removes all filesystem traces of secrets after deployment startup.
|
|
106
135
|
* Centralizes the defense-in-depth cleanup performed
|
package/src/cli/ssh.js
CHANGED
|
@@ -532,12 +532,28 @@ EOF`);
|
|
|
532
532
|
if (!host) throw new Error('copyDirToNode requires a host');
|
|
533
533
|
if (!localDir || !fs.existsSync(localDir)) throw new Error(`copyDirToNode: local dir not found: ${localDir}`);
|
|
534
534
|
if (!remoteDir) throw new Error('copyDirToNode requires a remoteDir');
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
535
|
+
try {
|
|
536
|
+
shellExec(`chmod 600 ${keyPath}`, { silent: true, silentOnError: true, disableLog: true });
|
|
537
|
+
const sshOpts = `-i ${keyPath} -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p ${port}`;
|
|
538
|
+
shellExec(`ssh ${sshOpts} ${user}@${host} 'mkdir -p ${remoteDir}'`, {
|
|
539
|
+
silent: true,
|
|
540
|
+
disableLog: true,
|
|
541
|
+
});
|
|
542
|
+
shellExec(`tar -C ${localDir} -c . | ssh ${sshOpts} ${user}@${host} 'tar -C ${remoteDir} -x'`, {
|
|
543
|
+
silent: true,
|
|
544
|
+
disableLog: true,
|
|
545
|
+
});
|
|
546
|
+
const fixups =
|
|
547
|
+
`${owner ? `chown -R ${owner} ${remoteDir}; ` : ''}${mode ? `chmod -R ${mode} ${remoteDir}` : ''}`.trim();
|
|
548
|
+
if (fixups)
|
|
549
|
+
shellExec(`ssh ${sshOpts} ${user}@${host} '${fixups}'`, {
|
|
550
|
+
silent: true,
|
|
551
|
+
disableLog: true,
|
|
552
|
+
});
|
|
553
|
+
} catch (err) {
|
|
554
|
+
logger.error(`copyDirToNode failed`);
|
|
555
|
+
process.exit(1);
|
|
556
|
+
}
|
|
541
557
|
},
|
|
542
558
|
|
|
543
559
|
/**
|
|
@@ -703,11 +719,14 @@ EOF
|
|
|
703
719
|
|
|
704
720
|
let last = { ok: false, code: 255, stdout: '', stderr: '', attempts: 0 };
|
|
705
721
|
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
706
|
-
const result = shellExec(
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
722
|
+
const result = shellExec(
|
|
723
|
+
`ssh ${sshOpts} ${user}@${host} bash -s <<'UNDERPOST_SSH_BATCH_EOF'\n${command}\nUNDERPOST_SSH_BATCH_EOF`,
|
|
724
|
+
{
|
|
725
|
+
stdout: false,
|
|
726
|
+
silentOnError: true,
|
|
727
|
+
disableLog: true,
|
|
728
|
+
},
|
|
729
|
+
);
|
|
711
730
|
last = {
|
|
712
731
|
ok: result.code === 0,
|
|
713
732
|
code: result.code,
|
|
@@ -425,8 +425,8 @@ class Modal {
|
|
|
425
425
|
s(`.main-body-btn-ui-menu-menu`).classList.add('hide');
|
|
426
426
|
s(`.main-body-btn-ui-menu-close`).classList.remove('hide');
|
|
427
427
|
if (s(`.btn-bar-center-icon-menu`)) {
|
|
428
|
-
|
|
429
|
-
|
|
428
|
+
sa(`.btn-bar-center-icon-close`).forEach((el) => el.classList.remove('hide'));
|
|
429
|
+
sa(`.btn-bar-center-icon-menu`).forEach((el) => el.classList.add('hide'));
|
|
430
430
|
}
|
|
431
431
|
|
|
432
432
|
s(`.main-body-btn-container`).style[
|
|
@@ -449,8 +449,8 @@ class Modal {
|
|
|
449
449
|
s(`.main-body-btn-ui-menu-close`).classList.add('hide');
|
|
450
450
|
s(`.main-body-btn-ui-menu-menu`).classList.remove('hide');
|
|
451
451
|
if (s(`.btn-bar-center-icon-menu`)) {
|
|
452
|
-
|
|
453
|
-
|
|
452
|
+
sa(`.btn-bar-center-icon-menu`).forEach((el) => el.classList.remove('hide'));
|
|
453
|
+
sa(`.btn-bar-center-icon-close`).forEach((el) => el.classList.add('hide'));
|
|
454
454
|
}
|
|
455
455
|
s(`.main-body-btn-container`).style[
|
|
456
456
|
true || (options.mode && options.mode.match('right')) ? 'right' : 'left'
|
|
@@ -683,6 +683,34 @@ class Modal {
|
|
|
683
683
|
>
|
|
684
684
|
</div>`
|
|
685
685
|
: ''}
|
|
686
|
+
${idModal === 'modal-menu' && options.mode !== 'slide-menu-right'
|
|
687
|
+
? html`<div
|
|
688
|
+
class="abs main-btn-menu-top-container"
|
|
689
|
+
style="bottom: 0px; left: 0px; z-index: 10; height: ${originHeightTopBar}px; width: ${originHeightTopBar}px"
|
|
690
|
+
>
|
|
691
|
+
${await BtnIcon.instance({
|
|
692
|
+
style: `height: 100%`,
|
|
693
|
+
class: `in fll main-btn-menu-top action-bar-box action-btn-center-top`,
|
|
694
|
+
label: html`<div class="abs center">
|
|
695
|
+
<i class="far fa-square btn-bar-center-icon-square hide"></i>
|
|
696
|
+
<span class="btn-bar-center-icon-close hide">${barConfig.buttons.close.label}</span>
|
|
697
|
+
<span class="btn-bar-center-icon-menu">${barConfig.buttons.menu.label}</span>
|
|
698
|
+
</div>`,
|
|
699
|
+
})}
|
|
700
|
+
</div>
|
|
701
|
+
|
|
702
|
+
<style>
|
|
703
|
+
.a-link-top-banner {
|
|
704
|
+
padding-left: 35px;
|
|
705
|
+
}
|
|
706
|
+
.main-body-btn-bar-custom {
|
|
707
|
+
top: 50px !important;
|
|
708
|
+
}
|
|
709
|
+
.main-body-btn-menu {
|
|
710
|
+
display: none;
|
|
711
|
+
}
|
|
712
|
+
</style>`
|
|
713
|
+
: ''}
|
|
686
714
|
</div>`,
|
|
687
715
|
);
|
|
688
716
|
EventsUI.onClick(`.action-btn-profile-log-in`, () => {
|
|
@@ -697,6 +725,12 @@ class Modal {
|
|
|
697
725
|
}
|
|
698
726
|
s(`.main-btn-sign-up`).click();
|
|
699
727
|
});
|
|
728
|
+
if (idModal === 'modal-menu' && options.mode !== 'slide-menu-right') {
|
|
729
|
+
EventsUI.onClick(`.action-btn-center-top`, (e) => {
|
|
730
|
+
e.preventDefault();
|
|
731
|
+
Modal.actionBtnCenter();
|
|
732
|
+
});
|
|
733
|
+
}
|
|
700
734
|
s(`.input-info-${inputSearchBoxId}`).style.textAlign = 'left';
|
|
701
735
|
htmls(`.input-info-${inputSearchBoxId}`, '');
|
|
702
736
|
const inputInfoNode = s(`.input-info-${inputSearchBoxId}`).cloneNode(true);
|
package/src/index.js
CHANGED
package/src/server/catalog.js
CHANGED
package/src/server/conf.js
CHANGED
|
@@ -476,9 +476,6 @@ const loadConf = (deployId = DEFAULT_DEPLOY_ID, subConf) => {
|
|
|
476
476
|
fs.removeSync(`${path}/.env.production`);
|
|
477
477
|
fs.removeSync(`${path}/.env.development`);
|
|
478
478
|
fs.removeSync(`${path}/.env.test`);
|
|
479
|
-
if (fs.existsSync(`${path}/typedoc.json`)) shellExec(`git checkout ${path}/typedoc.json`);
|
|
480
|
-
shellExec(`git checkout ${path}/package.json`);
|
|
481
|
-
shellExec(`git checkout ${path}/package-lock.json`);
|
|
482
479
|
return;
|
|
483
480
|
}
|
|
484
481
|
const folder = getConfFolder(deployId);
|
|
@@ -1775,6 +1772,165 @@ const loadConfServerJson = (jsonPath, options) => {
|
|
|
1775
1772
|
return options && options.resolve === true ? resolveConfSecrets(raw) : raw;
|
|
1776
1773
|
};
|
|
1777
1774
|
|
|
1775
|
+
/**
|
|
1776
|
+
* @method deepReplaceToken
|
|
1777
|
+
* @description Recursively replaces every occurrence of `from` with `to` in all
|
|
1778
|
+
* string leaves of a JSON-shaped value, returning a new structure.
|
|
1779
|
+
* @param {*} value - Any JSON-serialisable value.
|
|
1780
|
+
* @param {string} from - Token to replace.
|
|
1781
|
+
* @param {string} to - Replacement token.
|
|
1782
|
+
* @returns {*} A structurally identical value with the token replaced.
|
|
1783
|
+
* @memberof ServerConfBuilder
|
|
1784
|
+
*/
|
|
1785
|
+
const deepReplaceToken = (value, from, to) => {
|
|
1786
|
+
if (typeof value === 'string') return value.split(from).join(to);
|
|
1787
|
+
if (Array.isArray(value)) return value.map((entry) => deepReplaceToken(entry, from, to));
|
|
1788
|
+
if (value && typeof value === 'object')
|
|
1789
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, deepReplaceToken(v, from, to)]));
|
|
1790
|
+
return value;
|
|
1791
|
+
};
|
|
1792
|
+
|
|
1793
|
+
/**
|
|
1794
|
+
* @method readConfInstances
|
|
1795
|
+
* @description Reads `conf.instances.json` and returns the bare array of instance
|
|
1796
|
+
* entries. The canonical shape is a plain array; the historical `{ instances }`
|
|
1797
|
+
* object wrapper is still unwrapped for backward compatibility. Multi-instance is
|
|
1798
|
+
* declared per entry under `entry.multiInstance` (not deploy-wide).
|
|
1799
|
+
* @param {string} deployId - Deployment identifier (e.g. `dd-cyberia`).
|
|
1800
|
+
* @returns {Array<object>} Instance entries.
|
|
1801
|
+
* @memberof ServerConfBuilder
|
|
1802
|
+
*/
|
|
1803
|
+
const readConfInstances = (deployId) => {
|
|
1804
|
+
const raw = JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.instances.json`, 'utf8'));
|
|
1805
|
+
return Array.isArray(raw) ? raw : raw.instances || [];
|
|
1806
|
+
};
|
|
1807
|
+
|
|
1808
|
+
/**
|
|
1809
|
+
* @method loadInstanceTopology
|
|
1810
|
+
* @description Returns `{ default, variants }` describing the deploy's instance
|
|
1811
|
+
* variants, or `null` when the deploy is single-instance. The topology is
|
|
1812
|
+
* declared per instance entry (`entry.multiInstance.default` / `.variants`); all
|
|
1813
|
+
* multi-instance entries share the same variant set, so this returns it from the
|
|
1814
|
+
* first entry that declares one.
|
|
1815
|
+
* @param {string} deployId - Deployment identifier (e.g. `dd-cyberia`).
|
|
1816
|
+
* @returns {?{default: string, variants: Array<object>}} The topology, or `null`.
|
|
1817
|
+
* @memberof ServerConfBuilder
|
|
1818
|
+
*/
|
|
1819
|
+
const loadInstanceTopology = (deployId) => {
|
|
1820
|
+
for (const entry of readConfInstances(deployId)) {
|
|
1821
|
+
const mi = entry.multiInstance;
|
|
1822
|
+
if (mi && Array.isArray(mi.variants) && mi.variants.length > 0)
|
|
1823
|
+
return { default: mi.default || mi.variants[0].code, variants: mi.variants };
|
|
1824
|
+
}
|
|
1825
|
+
return null;
|
|
1826
|
+
};
|
|
1827
|
+
|
|
1828
|
+
/**
|
|
1829
|
+
* @method resolveInstanceEnvValue
|
|
1830
|
+
* @description Resolves one declared env value. A plain string is used verbatim;
|
|
1831
|
+
* an object is treated as env-scoped (`{ development, production }`), matching the
|
|
1832
|
+
* convention already used by `lifecycle` / `readinessProbe` / `livenessProbe`.
|
|
1833
|
+
* Placeholders are substituted from the variant tokens.
|
|
1834
|
+
* @param {string|object} value - Declared value.
|
|
1835
|
+
* @param {string} env - `development` | `production`.
|
|
1836
|
+
* @param {Object<string,string>} tokens - Placeholder name → replacement.
|
|
1837
|
+
* @returns {?string} Resolved value, or `null` when the env has no entry.
|
|
1838
|
+
* @memberof ServerConfBuilder
|
|
1839
|
+
*/
|
|
1840
|
+
const resolveInstanceEnvValue = (value, env, tokens) => {
|
|
1841
|
+
const scoped = value && typeof value === 'object' && !Array.isArray(value) ? value[env] : value;
|
|
1842
|
+
if (scoped === undefined || scoped === null) return null;
|
|
1843
|
+
return `${scoped}`.replace(/\{\{(\w+)\}\}/g, (match, token) => (token in tokens ? tokens[token] : match));
|
|
1844
|
+
};
|
|
1845
|
+
|
|
1846
|
+
/**
|
|
1847
|
+
* @method loadConfInstances
|
|
1848
|
+
* @description Loads `conf.instances.json` and expands every entry carrying a
|
|
1849
|
+
* `multiInstance` block into one concrete instance per declared variant.
|
|
1850
|
+
*
|
|
1851
|
+
* A template entry is never deployed as-is once variants exist: each variant
|
|
1852
|
+
* produces its own entry whose id, env file path, volume mount and
|
|
1853
|
+
* container-status strings are derived by replacing the template id token
|
|
1854
|
+
* throughout. The variant whose `slug` is empty keeps the template id verbatim,
|
|
1855
|
+
* so pre-existing deployments, PVCs and env directories survive the move to
|
|
1856
|
+
* multi-instance untouched.
|
|
1857
|
+
*
|
|
1858
|
+
* Nothing here is application-specific: the variant set (`default` + `variants`),
|
|
1859
|
+
* which env keys an instance needs (`env`), and prefix-stripping (`stripPathPrefix`)
|
|
1860
|
+
* are all declared per entry under `entry.multiInstance`. Env values may use the
|
|
1861
|
+
* `{{code}}`, `{{slug}}`, `{{path}}`, `{{id}}` and `{{default}}` placeholders and
|
|
1862
|
+
* may be env-scoped objects.
|
|
1863
|
+
*
|
|
1864
|
+
* Every expanded entry carries three extra fields consumed by the deploy runners:
|
|
1865
|
+
* `instanceCode` (the variant this instance serves), `templateId` (so targeting
|
|
1866
|
+
* the template id acts on the whole family) and `instanceEnv` (the resolved env
|
|
1867
|
+
* keys to write, keyed by environment: `{ development, production }`, so a build
|
|
1868
|
+
* in either mode can emit a complete env directory).
|
|
1869
|
+
*
|
|
1870
|
+
* @param {string} deployId - Deployment identifier (e.g. `dd-cyberia`).
|
|
1871
|
+
* @returns {Array<object>} Expanded instance entries.
|
|
1872
|
+
* @memberof ServerConfBuilder
|
|
1873
|
+
*/
|
|
1874
|
+
const INSTANCE_ENVS = ['development', 'production'];
|
|
1875
|
+
|
|
1876
|
+
const loadConfInstances = (deployId) => {
|
|
1877
|
+
const expanded = [];
|
|
1878
|
+
for (const entry of readConfInstances(deployId)) {
|
|
1879
|
+
const spec = entry.multiInstance;
|
|
1880
|
+
const variants = spec?.variants;
|
|
1881
|
+
if (!Array.isArray(variants) || 0 === variants.length) {
|
|
1882
|
+
expanded.push(entry);
|
|
1883
|
+
continue;
|
|
1884
|
+
}
|
|
1885
|
+
for (const variant of variants) {
|
|
1886
|
+
const id = variant.slug ? `${entry.id}-${variant.slug}` : entry.id;
|
|
1887
|
+
const instance = variant.slug ? deepReplaceToken(entry, entry.id, id) : JSON.parse(JSON.stringify(entry));
|
|
1888
|
+
delete instance.multiInstance;
|
|
1889
|
+
instance.id = id;
|
|
1890
|
+
instance.path = variant.path;
|
|
1891
|
+
instance.instanceCode = variant.code;
|
|
1892
|
+
instance.templateId = entry.id;
|
|
1893
|
+
|
|
1894
|
+
const tokens = {
|
|
1895
|
+
code: variant.code,
|
|
1896
|
+
slug: variant.slug || '',
|
|
1897
|
+
path: variant.path,
|
|
1898
|
+
id,
|
|
1899
|
+
default: spec.default || '',
|
|
1900
|
+
};
|
|
1901
|
+
// Resolve the declared env keys for both environments so a build in either
|
|
1902
|
+
// mode writes a complete env directory (development.env + production.env).
|
|
1903
|
+
instance.instanceEnv = Object.fromEntries(INSTANCE_ENVS.map((e) => [e, {}]));
|
|
1904
|
+
for (const [key, value] of Object.entries(spec.env || {}))
|
|
1905
|
+
for (const e of INSTANCE_ENVS) {
|
|
1906
|
+
const resolved = resolveInstanceEnvValue(value, e, tokens);
|
|
1907
|
+
if (resolved !== null) instance.instanceEnv[e][key] = resolved;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
// A backend that serves its routes at the root knows nothing about the
|
|
1911
|
+
// variant prefix — it is selected by env instead. Strip the prefix at the
|
|
1912
|
+
// proxy so the runtime stays instance-agnostic.
|
|
1913
|
+
if (spec.stripPathPrefix && variant.path !== '/')
|
|
1914
|
+
instance.pathRewritePolicy = [{ prefix: variant.path, replacement: '/' }];
|
|
1915
|
+
expanded.push(instance);
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
return expanded;
|
|
1919
|
+
};
|
|
1920
|
+
|
|
1921
|
+
/**
|
|
1922
|
+
* @method selectConfInstances
|
|
1923
|
+
* @description Filters expanded instances for a deploy target. Targeting a
|
|
1924
|
+
* template id (`mmo-server`) selects the whole instance family; targeting a
|
|
1925
|
+
* concrete id (`mmo-server-forest`) selects just that one.
|
|
1926
|
+
* @param {Array<object>} instances - Expanded entries from {@link loadConfInstances}.
|
|
1927
|
+
* @param {string} id - Target instance id or template id.
|
|
1928
|
+
* @returns {Array<object>} Matching entries.
|
|
1929
|
+
* @memberof ServerConfBuilder
|
|
1930
|
+
*/
|
|
1931
|
+
const selectConfInstances = (instances, id) =>
|
|
1932
|
+
instances.filter((instance) => instance.id === id || instance.templateId === id);
|
|
1933
|
+
|
|
1778
1934
|
/**
|
|
1779
1935
|
* Creates and writes the /etc/hosts file for a deployment.
|
|
1780
1936
|
* @method etcHostFactory
|
|
@@ -2103,6 +2259,10 @@ git add .`);
|
|
|
2103
2259
|
export {
|
|
2104
2260
|
Config,
|
|
2105
2261
|
loadConf,
|
|
2262
|
+
loadConfInstances,
|
|
2263
|
+
loadInstanceTopology,
|
|
2264
|
+
readConfInstances,
|
|
2265
|
+
selectConfInstances,
|
|
2106
2266
|
loadReplicas,
|
|
2107
2267
|
cloneConf,
|
|
2108
2268
|
getCapVariableName,
|
package/src/server/downloader.js
CHANGED
|
@@ -41,18 +41,18 @@ class Downloader {
|
|
|
41
41
|
const writer = fs.createWriteStream(fullPath);
|
|
42
42
|
response.data.pipe(writer);
|
|
43
43
|
writer.on('finish', () => {
|
|
44
|
-
logger.info('Download
|
|
44
|
+
logger.info('Download completet');
|
|
45
45
|
return resolve(fullPath);
|
|
46
46
|
});
|
|
47
47
|
writer.on('error', (error) => {
|
|
48
|
-
logger.error(
|
|
48
|
+
logger.error('Error downloading the file');
|
|
49
49
|
// Cleanup incomplete file if possible
|
|
50
50
|
if (fs.existsSync(fullPath)) fs.unlinkSync(fullPath);
|
|
51
51
|
return reject(error);
|
|
52
52
|
});
|
|
53
53
|
})
|
|
54
54
|
.catch((error) => {
|
|
55
|
-
logger.error(
|
|
55
|
+
logger.error('Error in the request');
|
|
56
56
|
return reject(error);
|
|
57
57
|
}),
|
|
58
58
|
);
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express middleware and controller/router helpers for engine APIs.
|
|
3
|
+
*
|
|
4
|
+
* @module src/server/middlewares.js
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { loggerFactory } from './logger.js';
|
|
8
|
+
import { moderatorGuard, adminGuard } from './auth.js';
|
|
9
|
+
|
|
10
|
+
const logger = loggerFactory(import.meta);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The public-read CORS policy: reflect the request origin (or allow any)
|
|
14
|
+
* and mark the resource embeddable cross-origin.
|
|
15
|
+
* @param {import('express').Request} req
|
|
16
|
+
* @param {import('express').Response} res
|
|
17
|
+
*/
|
|
18
|
+
const setCrossOriginHeaders = (req, res) => {
|
|
19
|
+
if (req && req.headers && req.headers.origin) res.set('Access-Control-Allow-Origin', req.headers.origin);
|
|
20
|
+
else res.setHeader('Access-Control-Allow-Origin', '*');
|
|
21
|
+
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Express middleware form of {@link setCrossOriginHeaders}. */
|
|
25
|
+
const crossOriginMiddleware = (req, res, next) => {
|
|
26
|
+
setCrossOriginHeaders(req, res);
|
|
27
|
+
next();
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Shallow request copy with `page`/`limit` parsed to integers.
|
|
32
|
+
* `path` and `params` are copied explicitly because spreading an Express
|
|
33
|
+
* request drops prototype getters.
|
|
34
|
+
* @param {import('express').Request} req
|
|
35
|
+
*/
|
|
36
|
+
const withParsedPagination = (req) => {
|
|
37
|
+
const { page, limit } = req.query;
|
|
38
|
+
return {
|
|
39
|
+
...req,
|
|
40
|
+
path: req.path,
|
|
41
|
+
params: req.params,
|
|
42
|
+
query: { ...req.query, page: parseInt(page), limit: parseInt(limit) },
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const sendSuccess = (res, data) => res.status(200).json({ status: 'success', data });
|
|
47
|
+
|
|
48
|
+
const sendError = (res, error, status = 400) => res.status(status).json({ status: 'error', message: error.message });
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Binary response with cross-origin and content headers.
|
|
52
|
+
* @param {import('express').Request} req
|
|
53
|
+
* @param {import('express').Response} res
|
|
54
|
+
* @param {{ buffer: Buffer, mimetype: string, filename: string, disposition?: 'inline'|'attachment' }} blob
|
|
55
|
+
*/
|
|
56
|
+
const sendBlob = (req, res, { buffer, mimetype, filename, disposition = 'inline' }) => {
|
|
57
|
+
setCrossOriginHeaders(req, res);
|
|
58
|
+
res.setHeader('Content-Type', mimetype);
|
|
59
|
+
res.setHeader('Content-Length', buffer.length);
|
|
60
|
+
res.setHeader('Content-Disposition', `${disposition}; filename="${filename}"`);
|
|
61
|
+
return res.status(200).end(buffer);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Wraps a controller body with error logging and the error response envelope.
|
|
66
|
+
* @param {(req, res, options) => Promise<any>} fn
|
|
67
|
+
* @param {{ errorStatus?: number }} [config]
|
|
68
|
+
*/
|
|
69
|
+
const controllerHandler =
|
|
70
|
+
(fn, { errorStatus = 400 } = {}) =>
|
|
71
|
+
async (req, res, options) => {
|
|
72
|
+
try {
|
|
73
|
+
return await fn(req, res, options);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
logger.error(error, error.stack);
|
|
76
|
+
return sendError(res, error, errorStatus);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Builds a controller method that delegates to a service method and wraps the
|
|
82
|
+
* result in the success envelope.
|
|
83
|
+
* @param {(req, res, options) => Promise<any>} serviceFn
|
|
84
|
+
* @param {{ errorStatus?: number, crossOrigin?: boolean, pagination?: boolean }} [config]
|
|
85
|
+
*/
|
|
86
|
+
const serviceHandler = (serviceFn, { errorStatus = 400, crossOrigin = false, pagination = false } = {}) =>
|
|
87
|
+
controllerHandler(
|
|
88
|
+
async (req, res, options) => {
|
|
89
|
+
if (crossOrigin) setCrossOriginHeaders(req, res);
|
|
90
|
+
const result = await serviceFn(pagination ? withParsedPagination(req) : req, res, options);
|
|
91
|
+
return sendSuccess(res, result);
|
|
92
|
+
},
|
|
93
|
+
{ errorStatus },
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Builds a standard CRUD controller class (static post/get/put/delete) from a
|
|
98
|
+
* service exposing the same methods. `get` parses pagination.
|
|
99
|
+
* @param {{ post, get, put, delete }} service
|
|
100
|
+
* @param {Object<string, Function>} [extend] - Extra or overriding static handlers.
|
|
101
|
+
*/
|
|
102
|
+
const buildCrudController = (service, extend = {}) => {
|
|
103
|
+
class CrudController {
|
|
104
|
+
static post = serviceHandler(service.post);
|
|
105
|
+
static get = serviceHandler(service.get, { pagination: true });
|
|
106
|
+
static put = serviceHandler(service.put);
|
|
107
|
+
static delete = serviceHandler(service.delete);
|
|
108
|
+
}
|
|
109
|
+
Object.assign(CrudController, extend);
|
|
110
|
+
return CrudController;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Registers the standard CRUD routes with the standard guard policy:
|
|
115
|
+
* public reads, moderator-guarded writes, admin-guarded collection delete.
|
|
116
|
+
* Custom routes must be registered before calling this (generic `/:id` routes
|
|
117
|
+
* capture everything).
|
|
118
|
+
* @param {import('express').Router} router
|
|
119
|
+
* @param {{ post, get, put, delete }} Controller
|
|
120
|
+
* @param {import('../../api/types.js').RouterOptions} options
|
|
121
|
+
* @param {{ readGuards?: Function[], writeGuards?: Function[], deleteAllGuards?: Function[] }} [config]
|
|
122
|
+
* Pass empty arrays for unguarded endpoints (e.g. player-written progress)
|
|
123
|
+
* or explicit guard chains (e.g. admin-only reads).
|
|
124
|
+
* @returns {import('express').Router}
|
|
125
|
+
*/
|
|
126
|
+
const registerCrudRoutes = (router, Controller, options, { readGuards = [], writeGuards, deleteAllGuards } = {}) => {
|
|
127
|
+
const write = writeGuards ?? [options.authMiddleware, moderatorGuard];
|
|
128
|
+
const deleteAll = deleteAllGuards ?? [options.authMiddleware, adminGuard];
|
|
129
|
+
const handle = (method) => async (req, res) => await Controller[method](req, res, options);
|
|
130
|
+
router.post(`/:id`, ...write, handle('post'));
|
|
131
|
+
router.post(`/`, ...write, handle('post'));
|
|
132
|
+
router.get(`/:id`, ...readGuards, handle('get'));
|
|
133
|
+
router.get(`/`, ...readGuards, handle('get'));
|
|
134
|
+
router.put(`/:id`, ...write, handle('put'));
|
|
135
|
+
router.put(`/`, ...write, handle('put'));
|
|
136
|
+
router.delete(`/:id`, ...write, handle('delete'));
|
|
137
|
+
router.delete(`/`, ...deleteAll, handle('delete'));
|
|
138
|
+
return router;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export {
|
|
142
|
+
setCrossOriginHeaders,
|
|
143
|
+
crossOriginMiddleware,
|
|
144
|
+
withParsedPagination,
|
|
145
|
+
sendSuccess,
|
|
146
|
+
sendError,
|
|
147
|
+
sendBlob,
|
|
148
|
+
controllerHandler,
|
|
149
|
+
serviceHandler,
|
|
150
|
+
buildCrudController,
|
|
151
|
+
registerCrudRoutes,
|
|
152
|
+
};
|