underpost 3.2.70 → 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/publish.ci.yml +3 -3
- package/.github/workflows/release.cd.yml +1 -1
- package/CHANGELOG.md +1177 -1038
- package/CLI-HELP.md +3 -1
- package/README.md +3 -3
- package/bin/build.js +10 -4
- package/docker-compose.yml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/package.json +10 -10
- 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 +31 -11
- package/src/cli/db.js +4 -2
- package/src/cli/deploy.js +51 -9
- package/src/cli/docker-compose.js +163 -9
- package/src/cli/fs.js +0 -1
- package/src/cli/image.js +25 -7
- package/src/cli/index.js +5 -0
- package/src/cli/release.js +4 -0
- package/src/cli/repository.js +13 -2
- package/src/cli/run.js +151 -78
- 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/conf.js +163 -0
- package/src/server/downloader.js +3 -3
- package/src/server/middlewares.js +152 -0
package/src/server/conf.js
CHANGED
|
@@ -1772,6 +1772,165 @@ const loadConfServerJson = (jsonPath, options) => {
|
|
|
1772
1772
|
return options && options.resolve === true ? resolveConfSecrets(raw) : raw;
|
|
1773
1773
|
};
|
|
1774
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
|
+
|
|
1775
1934
|
/**
|
|
1776
1935
|
* Creates and writes the /etc/hosts file for a deployment.
|
|
1777
1936
|
* @method etcHostFactory
|
|
@@ -2100,6 +2259,10 @@ git add .`);
|
|
|
2100
2259
|
export {
|
|
2101
2260
|
Config,
|
|
2102
2261
|
loadConf,
|
|
2262
|
+
loadConfInstances,
|
|
2263
|
+
loadInstanceTopology,
|
|
2264
|
+
readConfInstances,
|
|
2265
|
+
selectConfInstances,
|
|
2103
2266
|
loadReplicas,
|
|
2104
2267
|
cloneConf,
|
|
2105
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
|
+
};
|