plugin-cluster-manager 1.1.10 → 1.1.11
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/client.js +1 -0
- package/dist/client/Doctor.d.ts +2 -0
- package/dist/client/NginxCacheManager.d.ts +2 -0
- package/dist/client/index.js +1 -1
- package/dist/client/utils/clientSafeCache.d.ts +3 -0
- package/dist/client/utils/requestDedupInterceptor.d.ts +2 -0
- package/dist/externalVersion.js +5 -5
- package/dist/locale/en-US.json +97 -1
- package/dist/locale/vi-VN.json +98 -1
- package/dist/locale/zh-CN.json +98 -1
- package/dist/server/actions/cache-monitor.d.ts +10 -0
- package/dist/server/actions/cache-monitor.js +301 -0
- package/dist/server/actions/cluster-nodes.d.ts +15 -0
- package/dist/server/actions/cluster-nodes.js +394 -10
- package/dist/server/actions/doctor.d.ts +82 -0
- package/dist/server/actions/doctor.js +1250 -0
- package/dist/server/collections/cluster-manager-doctor-runs.d.ts +3 -0
- package/dist/server/collections/cluster-manager-doctor-runs.js +52 -0
- package/dist/server/collections/cluster-manager-doctor.d.ts +18 -0
- package/dist/server/collections/cluster-manager-doctor.js +44 -0
- package/dist/server/hooks/cacheInvalidationHooks.d.ts +1 -0
- package/dist/server/hooks/cacheInvalidationHooks.js +81 -0
- package/dist/server/middlewares/listMetaCacheMiddleware.d.ts +2 -0
- package/dist/server/middlewares/listMetaCacheMiddleware.js +79 -0
- package/dist/server/orchestrator/PackageManager.js +20 -16
- package/dist/server/plugin.js +61 -8
- package/dist/server/utils/versionManager.d.ts +10 -0
- package/dist/server/utils/versionManager.js +91 -0
- package/package.json +41 -41
- package/server.js +1 -0
- package/src/client/CacheMonitor.tsx +166 -179
- package/src/client/ClusterManagerLayout.tsx +48 -42
- package/src/client/ClusterNodes.tsx +691 -418
- package/src/client/Doctor.tsx +559 -0
- package/src/client/NginxCacheManager.tsx +415 -0
- package/src/client/PluginOperations.tsx +234 -234
- package/src/client/index.tsx +22 -14
- package/src/client/utils/clientSafeCache.ts +41 -0
- package/src/client/utils/requestDedupInterceptor.ts +213 -0
- package/src/locale/en-US.json +97 -1
- package/src/locale/vi-VN.json +98 -1
- package/src/locale/zh-CN.json +98 -1
- package/src/server/__tests__/doctor.test.ts +53 -0
- package/src/server/actions/acl-cache.ts +272 -272
- package/src/server/actions/cache-monitor.ts +453 -116
- package/src/server/actions/cluster-nodes.ts +882 -378
- package/src/server/actions/doctor.ts +1540 -0
- package/src/server/collections/cluster-manager-doctor-runs.ts +23 -0
- package/src/server/collections/cluster-manager-doctor.ts +19 -0
- package/src/server/hooks/cacheInvalidationHooks.ts +58 -0
- package/src/server/middlewares/listMetaCacheMiddleware.ts +55 -0
- package/src/server/orchestrator/PackageManager.ts +19 -15
- package/src/server/plugin.ts +338 -263
- package/src/server/utils/versionManager.ts +69 -0
|
@@ -40,7 +40,6 @@ __export(cluster_nodes_exports, {
|
|
|
40
40
|
readLocalLogs: () => readLocalLogs
|
|
41
41
|
});
|
|
42
42
|
module.exports = __toCommonJS(cluster_nodes_exports);
|
|
43
|
-
var import_server = require("@nocobase/server");
|
|
44
43
|
var import_os = __toESM(require("os"));
|
|
45
44
|
var import_fs = require("fs");
|
|
46
45
|
var import_path = __toESM(require("path"));
|
|
@@ -48,11 +47,162 @@ var import_crypto = __toESM(require("crypto"));
|
|
|
48
47
|
var import_redis_node_registry = require("../adapters/redis-node-registry");
|
|
49
48
|
var import_redis = require("../utils/redis");
|
|
50
49
|
var import_node = require("../utils/node");
|
|
50
|
+
var import_packages = require("../../shared/packages");
|
|
51
51
|
const LOG_RESPONSE_KEY_PREFIX = "cluster-manager:log-response:";
|
|
52
|
-
const
|
|
52
|
+
const LEGACY_MULTI_APP_PLUGINS = ["multi-app-manager", "multi-app-share-collection"];
|
|
53
53
|
function sleep(ms) {
|
|
54
54
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
55
55
|
}
|
|
56
|
+
function normalizeList(value) {
|
|
57
|
+
if (!Array.isArray(value)) return [];
|
|
58
|
+
return Array.from(
|
|
59
|
+
new Set(
|
|
60
|
+
value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean)
|
|
61
|
+
)
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
function normalizePackageMap(packages) {
|
|
65
|
+
return {
|
|
66
|
+
apt: normalizeList(packages == null ? void 0 : packages.apt),
|
|
67
|
+
npm: normalizeList(packages == null ? void 0 : packages.npm),
|
|
68
|
+
python: normalizeList(packages == null ? void 0 : packages.python)
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function parseCustomPackages(value) {
|
|
72
|
+
if (!value) {
|
|
73
|
+
return { python: [], node: [], npm: [] };
|
|
74
|
+
}
|
|
75
|
+
let customValue = value;
|
|
76
|
+
if (typeof customValue === "string") {
|
|
77
|
+
try {
|
|
78
|
+
customValue = JSON.parse(customValue);
|
|
79
|
+
} catch {
|
|
80
|
+
return { python: [], node: [], npm: [] };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!customValue || typeof customValue !== "object" || Array.isArray(customValue)) {
|
|
84
|
+
return { python: [], node: [], npm: [] };
|
|
85
|
+
}
|
|
86
|
+
const custom = customValue;
|
|
87
|
+
return {
|
|
88
|
+
python: normalizeList(custom.python),
|
|
89
|
+
node: normalizeList(custom.node),
|
|
90
|
+
npm: normalizeList(custom.npm)
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function parsePackageWhitelist(status) {
|
|
94
|
+
if (!(status == null ? void 0 : status.packageWhitelist)) {
|
|
95
|
+
return { apt: [], npm: [], python: [] };
|
|
96
|
+
}
|
|
97
|
+
let whitelistValue = status.packageWhitelist;
|
|
98
|
+
if (typeof whitelistValue === "string") {
|
|
99
|
+
try {
|
|
100
|
+
whitelistValue = JSON.parse(whitelistValue);
|
|
101
|
+
} catch {
|
|
102
|
+
return { apt: [], npm: [], python: [] };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (!whitelistValue || typeof whitelistValue !== "object" || Array.isArray(whitelistValue)) {
|
|
106
|
+
return { apt: [], npm: [], python: [] };
|
|
107
|
+
}
|
|
108
|
+
const whitelist = whitelistValue;
|
|
109
|
+
const npmPackages = [
|
|
110
|
+
...Array.isArray(whitelist.npm) ? whitelist.npm : [],
|
|
111
|
+
...Array.isArray(whitelist.node) ? whitelist.node : []
|
|
112
|
+
];
|
|
113
|
+
return {
|
|
114
|
+
apt: normalizeList(whitelist.apt),
|
|
115
|
+
npm: normalizeList(npmPackages),
|
|
116
|
+
python: normalizeList(whitelist.python)
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function diffPackages(expected, installed) {
|
|
120
|
+
return {
|
|
121
|
+
apt: expected.apt.filter((pkg) => !installed.apt.includes(pkg)),
|
|
122
|
+
npm: expected.npm.filter((pkg) => !installed.npm.includes(pkg)),
|
|
123
|
+
python: expected.python.filter((pkg) => !installed.python.includes(pkg))
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function hasMissingPackages(packages) {
|
|
127
|
+
return packages.apt.length > 0 || packages.npm.length > 0 || packages.python.length > 0;
|
|
128
|
+
}
|
|
129
|
+
function getErrorMessage(error) {
|
|
130
|
+
return error instanceof Error ? error.message : String(error);
|
|
131
|
+
}
|
|
132
|
+
function getNodeRole(node) {
|
|
133
|
+
if (node.isSandbox) {
|
|
134
|
+
return "sandbox";
|
|
135
|
+
}
|
|
136
|
+
const workerMode = node.workerMode || "main";
|
|
137
|
+
return workerMode === "worker" || workerMode === "task" || workerMode === "*" ? "worker" : "app";
|
|
138
|
+
}
|
|
139
|
+
function getReferenceVersion(nodes) {
|
|
140
|
+
var _a;
|
|
141
|
+
const appNode = nodes.find((node) => getNodeRole(node) === "app" && node.appVersion);
|
|
142
|
+
if (appNode == null ? void 0 : appNode.appVersion) {
|
|
143
|
+
return appNode.appVersion;
|
|
144
|
+
}
|
|
145
|
+
const counts = /* @__PURE__ */ new Map();
|
|
146
|
+
for (const node of nodes) {
|
|
147
|
+
if (!node.appVersion) continue;
|
|
148
|
+
counts.set(node.appVersion, (counts.get(node.appVersion) || 0) + 1);
|
|
149
|
+
}
|
|
150
|
+
return ((_a = [...counts.entries()].sort((a, b) => b[1] - a[1])[0]) == null ? void 0 : _a[0]) || null;
|
|
151
|
+
}
|
|
152
|
+
async function getClusterNodes(ctx) {
|
|
153
|
+
var _a, _b;
|
|
154
|
+
const plugin = (_b = (_a = ctx.app.pm) == null ? void 0 : _a.get) == null ? void 0 : _b.call(_a, "plugin-cluster-manager");
|
|
155
|
+
const registry = (plugin == null ? void 0 : plugin.nodeRegistry) ?? new import_redis_node_registry.RedisNodeRegistry(ctx.app);
|
|
156
|
+
return registry.getNodes();
|
|
157
|
+
}
|
|
158
|
+
async function getExpectedPackages(ctx) {
|
|
159
|
+
var _a;
|
|
160
|
+
const repo = ctx.db.getRepository("workerPackagesConfigs");
|
|
161
|
+
const config = await ((_a = repo == null ? void 0 : repo.findOne) == null ? void 0 : _a.call(repo));
|
|
162
|
+
if (!config) {
|
|
163
|
+
return normalizePackageMap((0, import_packages.packagesFromConfig)({}));
|
|
164
|
+
}
|
|
165
|
+
const configured = (0, import_packages.packagesFromConfig)({
|
|
166
|
+
aptPackages: config.get("aptPackages"),
|
|
167
|
+
pythonPackages: config.get("pythonPackages"),
|
|
168
|
+
npmPackages: config.get("npmPackages")
|
|
169
|
+
});
|
|
170
|
+
const custom = parseCustomPackages(config.get("customPackages"));
|
|
171
|
+
return normalizePackageMap({
|
|
172
|
+
apt: configured.apt,
|
|
173
|
+
npm: [...configured.npm || [], ...custom.node || [], ...custom.npm || []],
|
|
174
|
+
python: [...configured.python || [], ...custom.python || []]
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
async function readPackageStatus(ctx, node) {
|
|
178
|
+
const redis = (0, import_redis.getRedis)(ctx);
|
|
179
|
+
if (!redis) return null;
|
|
180
|
+
const keys = [
|
|
181
|
+
node.id ? `cluster-manager:pkg-status:${node.id}` : null,
|
|
182
|
+
node.hostname ? `orchestrator:pkg-status:${node.hostname}` : null,
|
|
183
|
+
node.name ? `orchestrator:pkg-status:${node.name}` : null
|
|
184
|
+
].filter(Boolean);
|
|
185
|
+
for (const key of keys) {
|
|
186
|
+
try {
|
|
187
|
+
const raw = await redis.sendCommand(["GET", key]);
|
|
188
|
+
if (raw && typeof raw === "string") {
|
|
189
|
+
return JSON.parse(raw);
|
|
190
|
+
}
|
|
191
|
+
} catch {
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
async function getApplicationPluginRows(ctx) {
|
|
197
|
+
const repo = ctx.db.getRepository("applicationPlugins");
|
|
198
|
+
if (!repo) return [];
|
|
199
|
+
const rows = await repo.find({ sort: ["name"] });
|
|
200
|
+
return rows.map((row) => row.toJSON());
|
|
201
|
+
}
|
|
202
|
+
function getPayload(ctx) {
|
|
203
|
+
var _a, _b, _c;
|
|
204
|
+
return ctx.action.params.values || ((_b = (_a = ctx.request) == null ? void 0 : _a.body) == null ? void 0 : _b.values) || ((_c = ctx.request) == null ? void 0 : _c.body) || {};
|
|
205
|
+
}
|
|
56
206
|
async function readLocalLogs(app, maxLines) {
|
|
57
207
|
const logBasePath = process.env.LOGGER_BASE_PATH || import_path.default.resolve(process.cwd(), "storage", "logs");
|
|
58
208
|
const appName = process.env.APP_NAME || app.name || "main";
|
|
@@ -129,9 +279,7 @@ const clusterActions = {
|
|
|
129
279
|
const plugin = (_b = (_a = ctx.app.pm) == null ? void 0 : _a.get) == null ? void 0 : _b.call(_a, "plugin-cluster-manager");
|
|
130
280
|
const registry = (plugin == null ? void 0 : plugin.nodeRegistry) ?? new import_redis_node_registry.RedisNodeRegistry(ctx.app);
|
|
131
281
|
const nodes = await registry.getNodes();
|
|
132
|
-
const appNode = nodes.find(
|
|
133
|
-
(n) => n.workerMode === "main" || n.workerMode === "" || n.workerMode === "app"
|
|
134
|
-
);
|
|
282
|
+
const appNode = nodes.find((n) => n.workerMode === "main" || n.workerMode === "" || n.workerMode === "app");
|
|
135
283
|
if (appNode == null ? void 0 : appNode.nodeDetails) {
|
|
136
284
|
ctx.body = appNode.nodeDetails;
|
|
137
285
|
} else {
|
|
@@ -173,12 +321,8 @@ const clusterActions = {
|
|
|
173
321
|
* Returns all known cluster environments/nodes (if discovery adapter supports it)
|
|
174
322
|
*/
|
|
175
323
|
async list(ctx, next) {
|
|
176
|
-
var _a, _b;
|
|
177
|
-
const supervisor = import_server.AppSupervisor.getInstance();
|
|
178
324
|
const environments = [];
|
|
179
|
-
const
|
|
180
|
-
const registry = (plugin == null ? void 0 : plugin.nodeRegistry) ?? new import_redis_node_registry.RedisNodeRegistry(ctx.app);
|
|
181
|
-
const nodes = await registry.getNodes();
|
|
325
|
+
const nodes = await getClusterNodes(ctx);
|
|
182
326
|
if (nodes && nodes.length > 0) {
|
|
183
327
|
for (const env of nodes) {
|
|
184
328
|
environments.push({
|
|
@@ -210,6 +354,168 @@ const clusterActions = {
|
|
|
210
354
|
ctx.body = { data: environments, meta: { count: environments.length } };
|
|
211
355
|
await next();
|
|
212
356
|
},
|
|
357
|
+
/**
|
|
358
|
+
* GET /clusterManagerCluster:drift
|
|
359
|
+
* Reports version/runtime/package drift across active cluster nodes.
|
|
360
|
+
*/
|
|
361
|
+
async drift(ctx, next) {
|
|
362
|
+
var _a, _b;
|
|
363
|
+
const nodes = await getClusterNodes(ctx);
|
|
364
|
+
const referenceVersion = getReferenceVersion(nodes);
|
|
365
|
+
const expectedPackages = await getExpectedPackages(ctx);
|
|
366
|
+
const versionDrifts = nodes.filter((node) => node.status !== "offline").filter((node) => referenceVersion && node.appVersion && node.appVersion !== referenceVersion).map((node) => ({
|
|
367
|
+
id: node.id,
|
|
368
|
+
name: node.name,
|
|
369
|
+
hostname: node.hostname,
|
|
370
|
+
role: getNodeRole(node),
|
|
371
|
+
expectedVersion: referenceVersion,
|
|
372
|
+
actualVersion: node.appVersion
|
|
373
|
+
}));
|
|
374
|
+
const runtimeReference = (_b = (_a = nodes.find((node) => getNodeRole(node) === "app")) == null ? void 0 : _a.nodeDetails) == null ? void 0 : _b.node;
|
|
375
|
+
const runtimeDrifts = runtimeReference ? nodes.filter((node) => node.status !== "offline").filter((node) => {
|
|
376
|
+
var _a2;
|
|
377
|
+
const runtime = (_a2 = node.nodeDetails) == null ? void 0 : _a2.node;
|
|
378
|
+
if (!runtime) return false;
|
|
379
|
+
return runtime.nodeVersion !== runtimeReference.nodeVersion || runtime.platform !== runtimeReference.platform || runtime.arch !== runtimeReference.arch;
|
|
380
|
+
}).map((node) => {
|
|
381
|
+
var _a2, _b2, _c, _d, _e, _f;
|
|
382
|
+
return {
|
|
383
|
+
id: node.id,
|
|
384
|
+
name: node.name,
|
|
385
|
+
hostname: node.hostname,
|
|
386
|
+
role: getNodeRole(node),
|
|
387
|
+
expected: {
|
|
388
|
+
nodeVersion: runtimeReference.nodeVersion,
|
|
389
|
+
platform: runtimeReference.platform,
|
|
390
|
+
arch: runtimeReference.arch
|
|
391
|
+
},
|
|
392
|
+
actual: {
|
|
393
|
+
nodeVersion: (_b2 = (_a2 = node.nodeDetails) == null ? void 0 : _a2.node) == null ? void 0 : _b2.nodeVersion,
|
|
394
|
+
platform: (_d = (_c = node.nodeDetails) == null ? void 0 : _c.node) == null ? void 0 : _d.platform,
|
|
395
|
+
arch: (_f = (_e = node.nodeDetails) == null ? void 0 : _e.node) == null ? void 0 : _f.arch
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
}) : [];
|
|
399
|
+
const packageDrifts = [];
|
|
400
|
+
for (const node of nodes.filter((item) => item.status !== "offline" && getNodeRole(item) !== "app")) {
|
|
401
|
+
const status = await readPackageStatus(ctx, node);
|
|
402
|
+
const installedPackages = parsePackageWhitelist(status);
|
|
403
|
+
const missingPackages = diffPackages(expectedPackages, installedPackages);
|
|
404
|
+
const hasPackageStatus = Boolean(status);
|
|
405
|
+
const statusOk = (status == null ? void 0 : status.initStatus) === "succeeded";
|
|
406
|
+
if (!hasPackageStatus || !statusOk || hasMissingPackages(missingPackages)) {
|
|
407
|
+
packageDrifts.push({
|
|
408
|
+
id: node.id,
|
|
409
|
+
name: node.name,
|
|
410
|
+
hostname: node.hostname,
|
|
411
|
+
role: getNodeRole(node),
|
|
412
|
+
status: (status == null ? void 0 : status.initStatus) || "unknown",
|
|
413
|
+
lastInitAt: (status == null ? void 0 : status.lastInitAt) || null,
|
|
414
|
+
missingPackages,
|
|
415
|
+
installedPackages,
|
|
416
|
+
initProgressLog: (status == null ? void 0 : status.initProgressLog) || ""
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
ctx.body = {
|
|
421
|
+
healthy: versionDrifts.length === 0 && runtimeDrifts.length === 0 && packageDrifts.length === 0,
|
|
422
|
+
referenceVersion,
|
|
423
|
+
expectedPackages,
|
|
424
|
+
versionDrifts,
|
|
425
|
+
runtimeDrifts,
|
|
426
|
+
packageDrifts,
|
|
427
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
428
|
+
summary: {
|
|
429
|
+
nodes: nodes.length,
|
|
430
|
+
versionDrifts: versionDrifts.length,
|
|
431
|
+
runtimeDrifts: runtimeDrifts.length,
|
|
432
|
+
packageDrifts: packageDrifts.length
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
await next();
|
|
436
|
+
},
|
|
437
|
+
/**
|
|
438
|
+
* GET /clusterManagerCluster:legacyDiagnostics
|
|
439
|
+
* Detects deprecated legacy multi-app plugins and leftover application records.
|
|
440
|
+
*/
|
|
441
|
+
async legacyDiagnostics(ctx, next) {
|
|
442
|
+
var _a, _b;
|
|
443
|
+
const rows = await getApplicationPluginRows(ctx);
|
|
444
|
+
const plugins = LEGACY_MULTI_APP_PLUGINS.map((name) => {
|
|
445
|
+
var _a2, _b2, _c, _d;
|
|
446
|
+
const row = rows.find((item) => item.name === name || item.packageName === `@nocobase/plugin-${name}`);
|
|
447
|
+
const loaded = Boolean(
|
|
448
|
+
((_b2 = (_a2 = ctx.app.pm) == null ? void 0 : _a2.get) == null ? void 0 : _b2.call(_a2, name)) || ((_d = (_c = ctx.app.pm) == null ? void 0 : _c.get) == null ? void 0 : _d.call(_c, `@nocobase/plugin-${name}`))
|
|
449
|
+
);
|
|
450
|
+
return {
|
|
451
|
+
name,
|
|
452
|
+
packageName: `@nocobase/plugin-${name}`,
|
|
453
|
+
installed: Boolean(row),
|
|
454
|
+
enabled: Boolean(row == null ? void 0 : row.enabled),
|
|
455
|
+
loaded,
|
|
456
|
+
version: row == null ? void 0 : row.version
|
|
457
|
+
};
|
|
458
|
+
});
|
|
459
|
+
let legacyApplicationCount = 0;
|
|
460
|
+
if ((_b = (_a = ctx.db).hasCollection) == null ? void 0 : _b.call(_a, "applications")) {
|
|
461
|
+
try {
|
|
462
|
+
legacyApplicationCount = await ctx.db.getRepository("applications").count();
|
|
463
|
+
} catch {
|
|
464
|
+
legacyApplicationCount = 0;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const findings = [];
|
|
468
|
+
const manager = plugins.find((plugin) => plugin.name === "multi-app-manager");
|
|
469
|
+
const shareCollection = plugins.find((plugin) => plugin.name === "multi-app-share-collection");
|
|
470
|
+
const appSupervisor = rows.find(
|
|
471
|
+
(item) => item.name === "app-supervisor" || item.packageName === "@nocobase/plugin-app-supervisor"
|
|
472
|
+
);
|
|
473
|
+
if ((manager == null ? void 0 : manager.enabled) || (manager == null ? void 0 : manager.loaded)) {
|
|
474
|
+
findings.push({
|
|
475
|
+
level: "warning",
|
|
476
|
+
code: "legacy_multi_app_manager_active",
|
|
477
|
+
messageKey: "Deprecated multi-app manager is active. It runs apps in shared process memory and should not be used for production cluster isolation.",
|
|
478
|
+
message: "Deprecated multi-app manager is active. It runs apps in shared process memory and should not be used for production cluster isolation."
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
if ((shareCollection == null ? void 0 : shareCollection.enabled) || (shareCollection == null ? void 0 : shareCollection.loaded)) {
|
|
482
|
+
findings.push({
|
|
483
|
+
level: "warning",
|
|
484
|
+
code: "legacy_share_collection_active",
|
|
485
|
+
messageKey: "Deprecated multi-app share collection is active. Avoid schema/table sharing for new cluster deployments.",
|
|
486
|
+
message: "Deprecated multi-app share collection is active. Avoid schema/table sharing for new cluster deployments."
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
if (legacyApplicationCount > 0) {
|
|
490
|
+
findings.push({
|
|
491
|
+
level: "warning",
|
|
492
|
+
code: "legacy_app_records_found",
|
|
493
|
+
messageKey: "{count} legacy application record(s) were found in the applications collection.",
|
|
494
|
+
messageArgs: { count: legacyApplicationCount },
|
|
495
|
+
message: `${legacyApplicationCount} legacy application record(s) were found in the applications collection.`
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
if (!(appSupervisor == null ? void 0 : appSupervisor.enabled)) {
|
|
499
|
+
findings.push({
|
|
500
|
+
level: "info",
|
|
501
|
+
code: "app_supervisor_not_enabled",
|
|
502
|
+
messageKey: "App Supervisor is not enabled. Use it for new multi-application management instead of deprecated multi-app plugins.",
|
|
503
|
+
message: "App Supervisor is not enabled. Use it for new multi-application management instead of deprecated multi-app plugins."
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
ctx.body = {
|
|
507
|
+
healthy: findings.every((finding) => finding.level !== "warning"),
|
|
508
|
+
plugins,
|
|
509
|
+
appSupervisor: appSupervisor ? {
|
|
510
|
+
installed: true,
|
|
511
|
+
enabled: Boolean(appSupervisor.enabled),
|
|
512
|
+
version: appSupervisor.version
|
|
513
|
+
} : { installed: false, enabled: false },
|
|
514
|
+
legacyApplicationCount,
|
|
515
|
+
findings
|
|
516
|
+
};
|
|
517
|
+
await next();
|
|
518
|
+
},
|
|
213
519
|
/**
|
|
214
520
|
* GET /clusterManagerCluster:health
|
|
215
521
|
* Health check for all subsystems
|
|
@@ -290,6 +596,84 @@ const clusterActions = {
|
|
|
290
596
|
}
|
|
291
597
|
await next();
|
|
292
598
|
},
|
|
599
|
+
/**
|
|
600
|
+
* POST /clusterManagerCluster:rollingRestart
|
|
601
|
+
* Restarts online nodes one-by-one, optionally filtered by role.
|
|
602
|
+
*/
|
|
603
|
+
async rollingRestart(ctx, next) {
|
|
604
|
+
const payload = getPayload(ctx);
|
|
605
|
+
const mode = payload.mode === "soft" ? "soft" : "hard";
|
|
606
|
+
const role = payload.role || "worker";
|
|
607
|
+
const delayMs = Math.min(Math.max(Number(payload.delayMs) || 5e3, 1e3), 6e4);
|
|
608
|
+
const requestedNodeIds = Array.isArray(payload.nodeIds) ? payload.nodeIds.map(String) : [];
|
|
609
|
+
const pubSub = ctx.app.pubSubManager;
|
|
610
|
+
if (!pubSub) {
|
|
611
|
+
ctx.throw(500, "PubSub manager is not initialized. HA requires PUBSUB_ADAPTER_REDIS_URL to be set.");
|
|
612
|
+
}
|
|
613
|
+
const nodes = (await getClusterNodes(ctx)).filter((node) => {
|
|
614
|
+
if (node.status === "offline") return false;
|
|
615
|
+
if (requestedNodeIds.length > 0) return node.id && requestedNodeIds.includes(node.id);
|
|
616
|
+
if (role === "all") return true;
|
|
617
|
+
return getNodeRole(node) === role;
|
|
618
|
+
});
|
|
619
|
+
if (nodes.length === 0) {
|
|
620
|
+
ctx.throw(404, "No online nodes match the rolling restart target.");
|
|
621
|
+
}
|
|
622
|
+
const myNodeId = (0, import_node.getLocalNodeId)(ctx.app);
|
|
623
|
+
const sortedNodes = nodes.sort((a, b) => {
|
|
624
|
+
if (a.id === myNodeId) return 1;
|
|
625
|
+
if (b.id === myNodeId) return -1;
|
|
626
|
+
return String(a.name || a.id).localeCompare(String(b.name || b.id));
|
|
627
|
+
});
|
|
628
|
+
const restartId = import_crypto.default.randomBytes(8).toString("hex");
|
|
629
|
+
const startedAt = Date.now();
|
|
630
|
+
const logger = ctx.app.logger;
|
|
631
|
+
const published = sortedNodes.map((node, index) => ({
|
|
632
|
+
id: node.id,
|
|
633
|
+
name: node.name,
|
|
634
|
+
hostname: node.hostname,
|
|
635
|
+
role: getNodeRole(node),
|
|
636
|
+
mode,
|
|
637
|
+
order: index + 1,
|
|
638
|
+
scheduledDelayMs: index * delayMs,
|
|
639
|
+
scheduledAt: new Date(startedAt + index * delayMs).toISOString()
|
|
640
|
+
}));
|
|
641
|
+
sortedNodes.forEach((node, index) => {
|
|
642
|
+
setTimeout(() => {
|
|
643
|
+
try {
|
|
644
|
+
const publishResult = pubSub.publish(
|
|
645
|
+
"cluster-manager:restart",
|
|
646
|
+
JSON.stringify({
|
|
647
|
+
restartId,
|
|
648
|
+
targetNodeId: node.id,
|
|
649
|
+
hostname: node.hostname,
|
|
650
|
+
mode
|
|
651
|
+
})
|
|
652
|
+
);
|
|
653
|
+
Promise.resolve(publishResult).catch((error) => {
|
|
654
|
+
logger.error(
|
|
655
|
+
`[ClusterManager] Failed to publish rolling restart ${restartId} for ${node.id || node.hostname}: ${getErrorMessage(error)}`
|
|
656
|
+
);
|
|
657
|
+
});
|
|
658
|
+
} catch (error) {
|
|
659
|
+
logger.error(
|
|
660
|
+
`[ClusterManager] Failed to schedule rolling restart ${restartId} for ${node.id || node.hostname}: ${getErrorMessage(error)}`
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
}, index * delayMs);
|
|
664
|
+
});
|
|
665
|
+
ctx.body = {
|
|
666
|
+
success: true,
|
|
667
|
+
restartId,
|
|
668
|
+
mode,
|
|
669
|
+
role,
|
|
670
|
+
delayMs,
|
|
671
|
+
scheduled: true,
|
|
672
|
+
estimatedDurationMs: Math.max(0, (sortedNodes.length - 1) * delayMs),
|
|
673
|
+
published
|
|
674
|
+
};
|
|
675
|
+
await next();
|
|
676
|
+
},
|
|
293
677
|
/**
|
|
294
678
|
* GET /clusterManagerCluster:logs?targetNodeId=xxx&lines=200
|
|
295
679
|
*
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { Context } from '@nocobase/actions';
|
|
3
|
+
import Application from '@nocobase/server';
|
|
4
|
+
interface DiagnosticLogLine {
|
|
5
|
+
source: string;
|
|
6
|
+
line: string;
|
|
7
|
+
timestamp?: string;
|
|
8
|
+
level?: string;
|
|
9
|
+
message?: string;
|
|
10
|
+
stack?: string;
|
|
11
|
+
}
|
|
12
|
+
interface LogSignature {
|
|
13
|
+
signature: string;
|
|
14
|
+
level: string;
|
|
15
|
+
count: number;
|
|
16
|
+
firstSeen?: string;
|
|
17
|
+
lastSeen?: string;
|
|
18
|
+
sources: string[];
|
|
19
|
+
samples: string[];
|
|
20
|
+
}
|
|
21
|
+
interface LogAnalysis {
|
|
22
|
+
totalLines: number;
|
|
23
|
+
levels: Record<string, number>;
|
|
24
|
+
signatures: LogSignature[];
|
|
25
|
+
}
|
|
26
|
+
interface PluginSnapshot {
|
|
27
|
+
name?: string;
|
|
28
|
+
packageName?: string;
|
|
29
|
+
enabled?: boolean;
|
|
30
|
+
dbVersion?: string;
|
|
31
|
+
loaded: boolean;
|
|
32
|
+
runtimeVersion?: string;
|
|
33
|
+
}
|
|
34
|
+
interface DoctorNodeSnapshot {
|
|
35
|
+
nodeId: string;
|
|
36
|
+
node: {
|
|
37
|
+
hostname: string;
|
|
38
|
+
pid: number;
|
|
39
|
+
workerMode: string;
|
|
40
|
+
role: string;
|
|
41
|
+
appVersion: string;
|
|
42
|
+
nodeVersion: string;
|
|
43
|
+
platform: string;
|
|
44
|
+
arch: string;
|
|
45
|
+
uptime: number;
|
|
46
|
+
isSandbox: boolean;
|
|
47
|
+
};
|
|
48
|
+
memory: NodeJS.MemoryUsage;
|
|
49
|
+
os: {
|
|
50
|
+
totalMemory: number;
|
|
51
|
+
freeMemory: number;
|
|
52
|
+
cpuCount: number;
|
|
53
|
+
loadAvg: number[];
|
|
54
|
+
};
|
|
55
|
+
env: Record<string, string | undefined>;
|
|
56
|
+
plugins: PluginSnapshot[];
|
|
57
|
+
logs: {
|
|
58
|
+
files: Array<{
|
|
59
|
+
file: string;
|
|
60
|
+
lineCount: number;
|
|
61
|
+
}>;
|
|
62
|
+
lines: DiagnosticLogLine[];
|
|
63
|
+
analysis: LogAnalysis;
|
|
64
|
+
};
|
|
65
|
+
collectedAt: string;
|
|
66
|
+
error?: string;
|
|
67
|
+
}
|
|
68
|
+
interface DoctorSnapshotOptions {
|
|
69
|
+
runId?: string;
|
|
70
|
+
sinceMs?: number;
|
|
71
|
+
untilMs?: number;
|
|
72
|
+
maxLines?: number;
|
|
73
|
+
}
|
|
74
|
+
export declare function collectLocalDoctorSnapshot(app: Application, options?: DoctorSnapshotOptions): Promise<DoctorNodeSnapshot>;
|
|
75
|
+
export declare const doctorActions: {
|
|
76
|
+
start(ctx: Context, next: () => Promise<void>): Promise<void>;
|
|
77
|
+
stop(ctx: Context, next: () => Promise<void>): Promise<void>;
|
|
78
|
+
status(ctx: Context, next: () => Promise<void>): Promise<void>;
|
|
79
|
+
report(ctx: Context, next: () => Promise<void>): Promise<void>;
|
|
80
|
+
download(ctx: Context, next: () => Promise<void>): Promise<void>;
|
|
81
|
+
};
|
|
82
|
+
export {};
|