plugin-cluster-manager 1.1.7 → 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/AclCacheManager.d.ts +2 -0
- package/dist/client/CacheMonitor.d.ts +2 -0
- package/dist/client/ClusterManagerLayout.d.ts +2 -0
- package/dist/client/ClusterNodes.d.ts +2 -0
- package/dist/client/ContainerOrchestrator.d.ts +2 -0
- package/dist/client/Doctor.d.ts +2 -0
- package/dist/client/EventQueueMonitor.d.ts +2 -0
- package/dist/client/LockMonitor.d.ts +2 -0
- package/dist/client/NginxCacheManager.d.ts +2 -0
- package/dist/client/PackageInstaller.d.ts +2 -0
- package/dist/client/PluginOperations.d.ts +2 -0
- package/dist/client/RedisMonitor.d.ts +2 -0
- package/dist/client/TaskManager.d.ts +2 -0
- package/dist/client/WorkflowExecutions.d.ts +2 -0
- package/dist/client/index.d.ts +5 -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/client/utils.d.ts +12 -0
- package/dist/externalVersion.js +5 -5
- package/dist/index.d.ts +2 -0
- 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/acl-cache.d.ts +53 -0
- package/dist/server/actions/acl-cache.js +1 -1
- package/dist/server/actions/cache-monitor.d.ts +33 -0
- package/dist/server/actions/cache-monitor.js +301 -0
- package/dist/server/actions/cluster-nodes.d.ts +64 -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/actions/event-queue-monitor.d.ts +13 -0
- package/dist/server/actions/lock-monitor.d.ts +19 -0
- package/dist/server/actions/orchestrator.d.ts +58 -0
- package/dist/server/actions/package-manager.d.ts +6 -0
- package/dist/server/actions/plugin-operations.d.ts +6 -0
- package/dist/server/actions/redis-monitor.d.ts +12 -0
- package/dist/server/actions/tasks.d.ts +7 -0
- package/dist/server/actions/workflow-executions.d.ts +7 -0
- package/dist/server/adapters/redis-lock-adapter.d.ts +15 -0
- package/dist/server/adapters/redis-node-registry.d.ts +12 -0
- package/dist/server/adapters/redis-pubsub-adapter.d.ts +16 -0
- package/dist/server/collections/app.d.ts +8 -0
- package/dist/server/collections/cluster-manager-acl-cache.d.ts +22 -0
- package/dist/server/collections/cluster-manager-cache-mgr.d.ts +22 -0
- package/dist/server/collections/cluster-manager-cluster.d.ts +22 -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/collections/cluster-manager-lock.d.ts +22 -0
- package/dist/server/collections/cluster-manager-plugins.d.ts +18 -0
- package/dist/server/collections/cluster-manager-queue.d.ts +22 -0
- package/dist/server/collections/cluster-manager-redis.d.ts +22 -0
- package/dist/server/collections/cluster-manager-workflow.d.ts +22 -0
- package/dist/server/collections/cluster-manager.d.ts +22 -0
- package/dist/server/collections/orchestrator-settings.d.ts +59 -0
- package/dist/server/collections/orchestrator-stacks.d.ts +102 -0
- package/dist/server/collections/worker-orchestrator.d.ts +22 -0
- package/dist/server/collections/worker-packages-configs.d.ts +3 -0
- package/dist/server/collections/worker-packages.d.ts +22 -0
- package/dist/server/hooks/cacheInvalidationHooks.d.ts +1 -0
- package/dist/server/hooks/cacheInvalidationHooks.js +81 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/middlewares/listMetaCacheMiddleware.d.ts +2 -0
- package/dist/server/middlewares/listMetaCacheMiddleware.js +79 -0
- package/dist/server/orchestrator/PackageManager.d.ts +39 -0
- package/dist/server/orchestrator/PackageManager.js +83 -27
- package/dist/server/orchestrator/docker-adapter.d.ts +41 -0
- package/dist/server/orchestrator/index.d.ts +4 -0
- package/dist/server/orchestrator/k8s-adapter.d.ts +50 -0
- package/dist/server/orchestrator/leader-election.d.ts +48 -0
- package/dist/server/orchestrator/types.d.ts +84 -0
- package/dist/server/plugin.d.ts +26 -0
- package/dist/server/plugin.js +70 -8
- package/dist/server/utils/node.d.ts +6 -0
- package/dist/server/utils/redis.d.ts +29 -0
- package/dist/server/utils/versionManager.d.ts +10 -0
- package/dist/server/utils/versionManager.js +91 -0
- package/dist/shared/packages.d.ts +23 -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 +353 -263
- package/src/server/utils/versionManager.ts +69 -0
package/src/server/plugin.ts
CHANGED
|
@@ -22,6 +22,9 @@ import { K8sAdapter } from './orchestrator/k8s-adapter';
|
|
|
22
22
|
import { LeaderElection } from './orchestrator/leader-election';
|
|
23
23
|
import { packageManagerActions } from './actions/package-manager';
|
|
24
24
|
import { PackageManager } from './orchestrator/PackageManager';
|
|
25
|
+
import { createListMetaCacheMiddleware } from './middlewares/listMetaCacheMiddleware';
|
|
26
|
+
import { registerCacheHooks } from './hooks/cacheInvalidationHooks';
|
|
27
|
+
import { collectLocalDoctorSnapshot, doctorActions } from './actions/doctor';
|
|
25
28
|
|
|
26
29
|
export class PluginClusterManagerServer extends Plugin {
|
|
27
30
|
public nodeRegistry: RedisNodeRegistry;
|
|
@@ -33,6 +36,21 @@ export class PluginClusterManagerServer extends Plugin {
|
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
async load() {
|
|
39
|
+
// Fix NocoBase core strategy resource permission check crash:
|
|
40
|
+
// Attachments collection has "createdBy: true" options but lacks explicit 'createdById' metadata field registration.
|
|
41
|
+
// When non-root roles upload attachments, core ACL merges 'own' filters (createdById) and calls checkFilterParams,
|
|
42
|
+
// which throws a NoPermissionError because getField('createdById') returns undefined.
|
|
43
|
+
// Registering 'createdById' explicitly as a metadata field on the attachments collection prevents this check from crashing.
|
|
44
|
+
this.db.extendCollection({
|
|
45
|
+
name: 'attachments',
|
|
46
|
+
fields: [
|
|
47
|
+
{
|
|
48
|
+
type: 'bigInt',
|
|
49
|
+
name: 'createdById',
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
});
|
|
53
|
+
|
|
36
54
|
this.nodeRegistry = new RedisNodeRegistry(this.app);
|
|
37
55
|
|
|
38
56
|
(this.app as any).on('afterStart', () => {
|
|
@@ -40,7 +58,12 @@ export class PluginClusterManagerServer extends Plugin {
|
|
|
40
58
|
|
|
41
59
|
// Automatically install packages on boot for worker nodes
|
|
42
60
|
const mode = process.env.WORKER_MODE || 'main';
|
|
43
|
-
const isWorker =
|
|
61
|
+
const isWorker =
|
|
62
|
+
mode === 'worker' ||
|
|
63
|
+
mode === 'task' ||
|
|
64
|
+
mode === '*' ||
|
|
65
|
+
process.env.APP_ROLE === 'worker' ||
|
|
66
|
+
process.env.APP_ROLE === 'sandbox';
|
|
44
67
|
if (isWorker) {
|
|
45
68
|
setTimeout(async () => {
|
|
46
69
|
try {
|
|
@@ -53,29 +76,31 @@ export class PluginClusterManagerServer extends Plugin {
|
|
|
53
76
|
}
|
|
54
77
|
if (config) {
|
|
55
78
|
this.app.logger.info('[ClusterManager] Auto-installing configured packages on worker boot...');
|
|
56
|
-
|
|
79
|
+
|
|
57
80
|
const { packagesFromConfig } = require('../shared/packages');
|
|
58
|
-
|
|
81
|
+
|
|
59
82
|
const configured = packagesFromConfig({
|
|
60
83
|
aptPackages: config.get('aptPackages'),
|
|
61
84
|
pythonPackages: config.get('pythonPackages'),
|
|
62
85
|
npmPackages: config.get('npmPackages'),
|
|
63
86
|
});
|
|
64
|
-
|
|
87
|
+
|
|
65
88
|
let custom = { python: [], node: [], npm: [] };
|
|
66
89
|
try {
|
|
67
90
|
const customRaw = config.get('customPackages');
|
|
68
91
|
if (customRaw) custom = typeof customRaw === 'string' ? JSON.parse(customRaw) : customRaw;
|
|
69
|
-
} catch {
|
|
70
|
-
|
|
92
|
+
} catch (err) {
|
|
93
|
+
// ignore
|
|
94
|
+
}
|
|
95
|
+
|
|
71
96
|
const unique = (arr: any[]) => Array.from(new Set(arr.filter(Boolean)));
|
|
72
|
-
|
|
97
|
+
|
|
73
98
|
const packages = {
|
|
74
99
|
apt: unique([...(configured.apt || [])]),
|
|
75
100
|
npm: unique([...(configured.npm || []), ...(custom.node || []), ...(custom.npm || [])]),
|
|
76
101
|
python: unique([...(configured.python || []), ...(custom.python || [])]),
|
|
77
102
|
};
|
|
78
|
-
|
|
103
|
+
|
|
79
104
|
const pm = new PackageManager(this.app);
|
|
80
105
|
await pm.executeInstall({
|
|
81
106
|
targetRole: 'all', // executeInstall will filter internally based on current role
|
|
@@ -84,7 +109,7 @@ export class PluginClusterManagerServer extends Plugin {
|
|
|
84
109
|
aptMirrorUrl: config.get('aptMirrorUrl'),
|
|
85
110
|
npmRegistryUrl: config.get('npmRegistryUrl'),
|
|
86
111
|
pypiIndexUrl: config.get('pypiIndexUrl'),
|
|
87
|
-
}
|
|
112
|
+
},
|
|
88
113
|
});
|
|
89
114
|
}
|
|
90
115
|
} catch (err: any) {
|
|
@@ -121,7 +146,7 @@ export class PluginClusterManagerServer extends Plugin {
|
|
|
121
146
|
if (lockMgr && lockMgr.registry && !lockMgr.registry.get('redis') && !lockMgr.adapters.get('redis')) {
|
|
122
147
|
lockMgr.registerAdapter('redis', {
|
|
123
148
|
Adapter: RedisLockAdapter,
|
|
124
|
-
options: { app: this.app }
|
|
149
|
+
options: { app: this.app },
|
|
125
150
|
});
|
|
126
151
|
this.app.logger.info('[ClusterManager] Polyfilled RedisLockAdapter as an active distributed lock provider');
|
|
127
152
|
}
|
|
@@ -130,7 +155,7 @@ export class PluginClusterManagerServer extends Plugin {
|
|
|
130
155
|
const pubSub = (this.app as any).pubSubManager;
|
|
131
156
|
if (pubSub) {
|
|
132
157
|
const myNodeId = getLocalNodeId(this.app);
|
|
133
|
-
|
|
158
|
+
|
|
134
159
|
// ── Log request handler: ONLY the targeted node receives this via dynamic channel ──
|
|
135
160
|
pubSub.subscribe(`cluster-manager:log-request:${myNodeId}`, async (msg: string) => {
|
|
136
161
|
try {
|
|
@@ -141,14 +166,56 @@ export class PluginClusterManagerServer extends Plugin {
|
|
|
141
166
|
|
|
142
167
|
const logData = await readLocalLogs(this.app, lines || 200);
|
|
143
168
|
const responseKey = `cluster-manager:log-response:${requestId}`;
|
|
144
|
-
await redis.sendCommand([
|
|
145
|
-
'SET', responseKey, JSON.stringify(logData), 'EX', '30',
|
|
146
|
-
]);
|
|
169
|
+
await redis.sendCommand(['SET', responseKey, JSON.stringify(logData), 'EX', '30']);
|
|
147
170
|
this.app.logger.debug(`[ClusterManager] Served log request ${requestId} for ${targetNodeId}`);
|
|
148
171
|
} catch (err: any) {
|
|
149
172
|
this.app.logger.error(`[ClusterManager] Error handling log request: ${err.message}`);
|
|
150
173
|
}
|
|
151
|
-
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
pubSub.subscribe(`cluster-manager:doctor-collect:${myNodeId}`, async (msg: string) => {
|
|
177
|
+
const redis = getRedisClient(this.app);
|
|
178
|
+
let requestId = '';
|
|
179
|
+
try {
|
|
180
|
+
const parsed = typeof msg === 'string' ? JSON.parse(msg) : msg;
|
|
181
|
+
requestId = parsed.requestId;
|
|
182
|
+
if (!redis || !requestId) return;
|
|
183
|
+
|
|
184
|
+
const snapshot = await collectLocalDoctorSnapshot(this.app, {
|
|
185
|
+
runId: parsed.runId,
|
|
186
|
+
sinceMs: parsed.sinceMs,
|
|
187
|
+
untilMs: parsed.untilMs,
|
|
188
|
+
maxLines: parsed.maxLines,
|
|
189
|
+
});
|
|
190
|
+
await redis.sendCommand([
|
|
191
|
+
'SET',
|
|
192
|
+
`cluster-manager:doctor-response:${requestId}`,
|
|
193
|
+
JSON.stringify(snapshot),
|
|
194
|
+
'EX',
|
|
195
|
+
'90',
|
|
196
|
+
]);
|
|
197
|
+
this.app.logger.debug(`[ClusterManager] Served doctor snapshot request ${requestId}`);
|
|
198
|
+
} catch (err: unknown) {
|
|
199
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
200
|
+
this.app.logger.error(`[ClusterManager] Error handling doctor snapshot request: ${message}`);
|
|
201
|
+
if (redis && requestId) {
|
|
202
|
+
const fallback = {
|
|
203
|
+
nodeId: getLocalNodeId(this.app),
|
|
204
|
+
collectedAt: new Date().toISOString(),
|
|
205
|
+
error: message,
|
|
206
|
+
};
|
|
207
|
+
await redis
|
|
208
|
+
.sendCommand([
|
|
209
|
+
'SET',
|
|
210
|
+
`cluster-manager:doctor-response:${requestId}`,
|
|
211
|
+
JSON.stringify(fallback),
|
|
212
|
+
'EX',
|
|
213
|
+
'90',
|
|
214
|
+
])
|
|
215
|
+
.catch(() => {});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
});
|
|
152
219
|
// Package installation handler. PubSub delivers this to every node, and PackageManager
|
|
153
220
|
// filters by the requested target role before executing anything locally.
|
|
154
221
|
pubSub.subscribe('cluster-manager.install-packages', async (payload: any) => {
|
|
@@ -162,85 +229,97 @@ export class PluginClusterManagerServer extends Plugin {
|
|
|
162
229
|
});
|
|
163
230
|
|
|
164
231
|
pubSub.subscribe('cluster-manager:restart', (msg: string) => {
|
|
165
|
-
try {
|
|
166
|
-
let target = msg;
|
|
167
|
-
let mode = 'hard';
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
//
|
|
203
|
-
this.app.resourcer.define({
|
|
204
|
-
name: '
|
|
205
|
-
actions:
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
//
|
|
209
|
-
this.app.resourcer.define({
|
|
210
|
-
name: '
|
|
211
|
-
actions:
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
//
|
|
215
|
-
this.app.resourcer.define({
|
|
216
|
-
name: '
|
|
217
|
-
actions:
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
//
|
|
221
|
-
this.app.resourcer.define({
|
|
222
|
-
name: '
|
|
223
|
-
actions:
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
//
|
|
227
|
-
this.app.resourcer.define({
|
|
228
|
-
name: '
|
|
229
|
-
actions:
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
//
|
|
233
|
-
this.app.resourcer.define({
|
|
234
|
-
name: '
|
|
235
|
-
actions:
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
//
|
|
239
|
-
this.app.resourcer.define({
|
|
240
|
-
name: '
|
|
241
|
-
actions:
|
|
242
|
-
});
|
|
243
|
-
|
|
232
|
+
try {
|
|
233
|
+
let target = msg;
|
|
234
|
+
let mode = 'hard';
|
|
235
|
+
let targetNodeId = '';
|
|
236
|
+
|
|
237
|
+
if (msg.startsWith('{')) {
|
|
238
|
+
const parsed = JSON.parse(msg);
|
|
239
|
+
target = parsed.hostname || parsed.target || '';
|
|
240
|
+
targetNodeId = parsed.targetNodeId || '';
|
|
241
|
+
mode = parsed.mode || 'hard';
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const myNodeId = getLocalNodeId(this.app);
|
|
245
|
+
const shouldRestart = targetNodeId ? targetNodeId === myNodeId : target === os.hostname() || target === '*';
|
|
246
|
+
if (shouldRestart) {
|
|
247
|
+
this.app.logger.warn(`[ClusterManager] Received ${mode} restart command for node ${os.hostname()}...`);
|
|
248
|
+
setTimeout(async () => {
|
|
249
|
+
try {
|
|
250
|
+
if (mode === 'soft') {
|
|
251
|
+
this.app.logger.warn(`[ClusterManager] Triggering NocoBase Soft Restart...`);
|
|
252
|
+
await this.app.restart();
|
|
253
|
+
} else {
|
|
254
|
+
this.app.logger.warn(`[ClusterManager] Shutting down Node.js process for Hard Restart...`);
|
|
255
|
+
await this.app.stop();
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
} catch (e: any) {
|
|
259
|
+
// ignore
|
|
260
|
+
}
|
|
261
|
+
}, 1000); // 1-second delay so HTTP API can gracefully respond first
|
|
262
|
+
}
|
|
263
|
+
} catch (err) {
|
|
264
|
+
this.app.logger.error(`[ClusterManager] Parse error for restart message: ${msg}`);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Task management (reads asyncTasks table)
|
|
270
|
+
this.app.resourcer.define({
|
|
271
|
+
name: 'clusterManager',
|
|
272
|
+
actions: tasksActions,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Workflow execution management (reads executions + jobs tables)
|
|
276
|
+
this.app.resourcer.define({
|
|
277
|
+
name: 'clusterManagerWorkflow',
|
|
278
|
+
actions: workflowActions,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Redis live metrics
|
|
282
|
+
this.app.resourcer.define({
|
|
283
|
+
name: 'clusterManagerRedis',
|
|
284
|
+
actions: redisActions,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// ACL cache management
|
|
288
|
+
this.app.resourcer.define({
|
|
289
|
+
name: 'clusterManagerAclCache',
|
|
290
|
+
actions: aclCacheActions,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Cluster nodes & health
|
|
294
|
+
this.app.resourcer.define({
|
|
295
|
+
name: 'clusterManagerCluster',
|
|
296
|
+
actions: clusterActions,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// Time-boxed diagnostic sessions and report download
|
|
300
|
+
this.app.resourcer.define({
|
|
301
|
+
name: 'clusterManagerDoctor',
|
|
302
|
+
actions: doctorActions,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Event queue monitoring
|
|
306
|
+
this.app.resourcer.define({
|
|
307
|
+
name: 'clusterManagerQueue',
|
|
308
|
+
actions: eventQueueActions,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Distributed lock monitoring
|
|
312
|
+
this.app.resourcer.define({
|
|
313
|
+
name: 'clusterManagerLock',
|
|
314
|
+
actions: lockActions,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// Cache manager monitoring
|
|
318
|
+
this.app.resourcer.define({
|
|
319
|
+
name: 'clusterManagerCacheMgr',
|
|
320
|
+
actions: cacheMonitorActions,
|
|
321
|
+
});
|
|
322
|
+
|
|
244
323
|
// Package manager (installs apt/npm/python packages across nodes)
|
|
245
324
|
this.app.resourcer.define({
|
|
246
325
|
name: 'workerPackages',
|
|
@@ -252,7 +331,7 @@ export class PluginClusterManagerServer extends Plugin {
|
|
|
252
331
|
name: 'clusterManagerPlugins',
|
|
253
332
|
actions: pluginOperationsActions,
|
|
254
333
|
});
|
|
255
|
-
|
|
334
|
+
|
|
256
335
|
// Install ACL cache middleware inside the ACL chain so cached permissions are not overwritten.
|
|
257
336
|
const aclCacheMiddleware = createAclCacheMiddleware(this.app);
|
|
258
337
|
(this.app as any).acl.use(aclCacheMiddleware, {
|
|
@@ -260,119 +339,130 @@ export class PluginClusterManagerServer extends Plugin {
|
|
|
260
339
|
before: 'core',
|
|
261
340
|
after: 'allow-manager',
|
|
262
341
|
});
|
|
263
|
-
|
|
264
|
-
//
|
|
265
|
-
this.app
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
342
|
+
|
|
343
|
+
// Install collections:listMeta resource cache middleware after setCurrentRole
|
|
344
|
+
const listMetaCacheMiddleware = createListMetaCacheMiddleware(this.app);
|
|
345
|
+
this.app.resourcer.use(listMetaCacheMiddleware, {
|
|
346
|
+
tag: 'listMetaCache',
|
|
347
|
+
after: 'setCurrentRole',
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// Register DB hooks for invalidating cache versions
|
|
351
|
+
registerCacheHooks(this.app);
|
|
352
|
+
|
|
353
|
+
// Lightweight healthcheck endpoint avoiding workflow pre-action and resourcer spam
|
|
354
|
+
this.app.use(async (ctx: any, next: any) => {
|
|
355
|
+
if (ctx.path === '/api/clusterManager:health' && (ctx.method === 'GET' || ctx.method === 'HEAD')) {
|
|
356
|
+
ctx.body = {
|
|
357
|
+
status: 'ok',
|
|
358
|
+
version: process.env.NOCOBASE_VERSION || process.version,
|
|
359
|
+
mode: process.env.WORKER_MODE || 'main',
|
|
360
|
+
};
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
await next();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Admin-only access
|
|
367
|
+
this.app.acl.registerSnippet({
|
|
368
|
+
name: `pm.${this.name}`,
|
|
369
|
+
actions: [
|
|
370
|
+
'clusterManager:*',
|
|
371
|
+
'clusterManagerWorkflow:*',
|
|
372
|
+
'clusterManagerRedis:*',
|
|
373
|
+
'clusterManagerAclCache:*',
|
|
374
|
+
'clusterManagerCluster:*',
|
|
375
|
+
'clusterManagerDoctor:*',
|
|
376
|
+
'clusterManagerQueue:*',
|
|
377
|
+
'clusterManagerLock:*',
|
|
378
|
+
'clusterManagerCacheMgr:*',
|
|
289
379
|
'workerOrchestrator:*',
|
|
290
380
|
'orchestratorStacks:*',
|
|
291
381
|
'workerPackages:*',
|
|
292
382
|
'clusterManagerPlugins:*',
|
|
293
383
|
],
|
|
294
384
|
});
|
|
295
|
-
|
|
296
|
-
// ── Container Orchestrator ──
|
|
297
|
-
await this.initOrchestrator();
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
private registerPubSubAdapter() {
|
|
301
|
-
const url = process.env.PUBSUB_ADAPTER_REDIS_URL;
|
|
302
|
-
if (!url) {
|
|
303
|
-
this.app.logger.info('[cluster-manager] PUBSUB_ADAPTER_REDIS_URL not set, skipping Redis PubSub adapter');
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Don't override if another plugin already set an adapter
|
|
308
|
-
const existingAdapter = (this.app.pubSubManager as any).adapter;
|
|
309
|
-
if (existingAdapter) {
|
|
310
|
-
this.app.logger.info('[cluster-manager] PubSub adapter already registered, skipping');
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const adapter = new RedisPubSubAdapter(url, this.app.logger);
|
|
315
|
-
this.app.pubSubManager.setAdapter(adapter);
|
|
316
|
-
this.app.logger.info('[cluster-manager] Redis PubSub adapter registered');
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Initialize the Container Orchestrator subsystem.
|
|
321
|
-
* Config is loaded from DB (orchestratorSettings collection) first,
|
|
322
|
-
* then falls back to ORCHESTRATOR_ADAPTER env var.
|
|
323
|
-
* This allows manual configuration via the NocoBase admin UI.
|
|
324
|
-
*/
|
|
325
|
-
private async initOrchestrator() {
|
|
326
|
-
// Always register actions + collections so the UI can configure settings
|
|
327
|
-
// even before an adapter is connected
|
|
328
|
-
this.app.resourcer.define({
|
|
329
|
-
name: 'workerOrchestrator',
|
|
330
|
-
actions: {
|
|
331
|
-
...orchestratorActions,
|
|
332
|
-
// Settings CRUD: read/write orchestrator config from DB
|
|
333
|
-
async getSettings(ctx: any, next: () => Promise<void>) {
|
|
334
|
-
try {
|
|
335
|
-
const repo = ctx.db.getRepository('orchestratorSettings');
|
|
336
|
-
let settings = await repo.findOne();
|
|
337
|
-
if (!settings) {
|
|
338
|
-
settings = await repo.create({
|
|
339
|
-
values: { adapterType: process.env.ORCHESTRATOR_ADAPTER || 'none' },
|
|
340
|
-
});
|
|
341
|
-
}
|
|
342
|
-
ctx.body = settings.toJSON();
|
|
343
|
-
} catch {
|
|
344
|
-
// Table may not exist during migration window
|
|
345
|
-
ctx.body = { adapterType: 'none', _note: 'Settings table not yet ready.' };
|
|
346
|
-
}
|
|
347
|
-
await next();
|
|
348
|
-
},
|
|
349
|
-
async saveSettings(ctx: any, next: () => Promise<void>) {
|
|
350
|
-
const values = ctx.action.params.values || {};
|
|
351
|
-
const repo = ctx.db.getRepository('orchestratorSettings');
|
|
352
|
-
let settings = await repo.findOne();
|
|
353
|
-
if (settings) {
|
|
354
|
-
await repo.update({ filterByTk: settings.get('id'), values });
|
|
355
|
-
} else {
|
|
356
|
-
settings = await repo.create({ values });
|
|
357
|
-
}
|
|
358
|
-
// Reinitialize adapter with new settings
|
|
359
|
-
const plugin = ctx.app.pm.get('plugin-cluster-manager') as PluginClusterManagerServer;
|
|
360
|
-
await plugin.connectAdapter(values);
|
|
361
|
-
ctx.body = { success: true, message: 'Settings saved. Adapter reinitialized.' };
|
|
362
|
-
await next();
|
|
363
|
-
},
|
|
364
|
-
},
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
// Load settings from DB and try to connect
|
|
368
|
-
try {
|
|
369
|
-
const repo = this.app.db.getRepository('orchestratorSettings');
|
|
370
|
-
const settings = await repo.findOne();
|
|
371
|
-
if (settings) {
|
|
372
|
-
await this.connectAdapter(settings.toJSON());
|
|
373
|
-
} else {
|
|
374
|
-
// Fall back to env var for initial setup
|
|
375
|
-
const envAdapter = process.env.ORCHESTRATOR_ADAPTER;
|
|
385
|
+
|
|
386
|
+
// ── Container Orchestrator ──
|
|
387
|
+
await this.initOrchestrator();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private registerPubSubAdapter() {
|
|
391
|
+
const url = process.env.PUBSUB_ADAPTER_REDIS_URL;
|
|
392
|
+
if (!url) {
|
|
393
|
+
this.app.logger.info('[cluster-manager] PUBSUB_ADAPTER_REDIS_URL not set, skipping Redis PubSub adapter');
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Don't override if another plugin already set an adapter
|
|
398
|
+
const existingAdapter = (this.app.pubSubManager as any).adapter;
|
|
399
|
+
if (existingAdapter) {
|
|
400
|
+
this.app.logger.info('[cluster-manager] PubSub adapter already registered, skipping');
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const adapter = new RedisPubSubAdapter(url, this.app.logger);
|
|
405
|
+
this.app.pubSubManager.setAdapter(adapter);
|
|
406
|
+
this.app.logger.info('[cluster-manager] Redis PubSub adapter registered');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Initialize the Container Orchestrator subsystem.
|
|
411
|
+
* Config is loaded from DB (orchestratorSettings collection) first,
|
|
412
|
+
* then falls back to ORCHESTRATOR_ADAPTER env var.
|
|
413
|
+
* This allows manual configuration via the NocoBase admin UI.
|
|
414
|
+
*/
|
|
415
|
+
private async initOrchestrator() {
|
|
416
|
+
// Always register actions + collections so the UI can configure settings
|
|
417
|
+
// even before an adapter is connected
|
|
418
|
+
this.app.resourcer.define({
|
|
419
|
+
name: 'workerOrchestrator',
|
|
420
|
+
actions: {
|
|
421
|
+
...orchestratorActions,
|
|
422
|
+
// Settings CRUD: read/write orchestrator config from DB
|
|
423
|
+
async getSettings(ctx: any, next: () => Promise<void>) {
|
|
424
|
+
try {
|
|
425
|
+
const repo = ctx.db.getRepository('orchestratorSettings');
|
|
426
|
+
let settings = await repo.findOne();
|
|
427
|
+
if (!settings) {
|
|
428
|
+
settings = await repo.create({
|
|
429
|
+
values: { adapterType: process.env.ORCHESTRATOR_ADAPTER || 'none' },
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
ctx.body = settings.toJSON();
|
|
433
|
+
} catch {
|
|
434
|
+
// Table may not exist during migration window
|
|
435
|
+
ctx.body = { adapterType: 'none', _note: 'Settings table not yet ready.' };
|
|
436
|
+
}
|
|
437
|
+
await next();
|
|
438
|
+
},
|
|
439
|
+
async saveSettings(ctx: any, next: () => Promise<void>) {
|
|
440
|
+
const values = ctx.action.params.values || {};
|
|
441
|
+
const repo = ctx.db.getRepository('orchestratorSettings');
|
|
442
|
+
let settings = await repo.findOne();
|
|
443
|
+
if (settings) {
|
|
444
|
+
await repo.update({ filterByTk: settings.get('id'), values });
|
|
445
|
+
} else {
|
|
446
|
+
settings = await repo.create({ values });
|
|
447
|
+
}
|
|
448
|
+
// Reinitialize adapter with new settings
|
|
449
|
+
const plugin = ctx.app.pm.get('plugin-cluster-manager') as PluginClusterManagerServer;
|
|
450
|
+
await plugin.connectAdapter(values);
|
|
451
|
+
ctx.body = { success: true, message: 'Settings saved. Adapter reinitialized.' };
|
|
452
|
+
await next();
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// Load settings from DB and try to connect
|
|
458
|
+
try {
|
|
459
|
+
const repo = this.app.db.getRepository('orchestratorSettings');
|
|
460
|
+
const settings = await repo.findOne();
|
|
461
|
+
if (settings) {
|
|
462
|
+
await this.connectAdapter(settings.toJSON());
|
|
463
|
+
} else {
|
|
464
|
+
// Fall back to env var for initial setup
|
|
465
|
+
const envAdapter = process.env.ORCHESTRATOR_ADAPTER;
|
|
376
466
|
if (envAdapter && envAdapter !== 'none') {
|
|
377
467
|
await this.connectAdapter({
|
|
378
468
|
adapterType: envAdapter,
|
|
@@ -383,10 +473,10 @@ export class PluginClusterManagerServer extends Plugin {
|
|
|
383
473
|
this.app.logger.info('[Orchestrator] No adapter configured — configurable via Cluster Manager UI');
|
|
384
474
|
}
|
|
385
475
|
}
|
|
386
|
-
} catch (err: any) {
|
|
387
|
-
this.app.logger.warn(`[Orchestrator] Could not load settings: ${err.message}`);
|
|
388
|
-
}
|
|
389
|
-
|
|
476
|
+
} catch (err: any) {
|
|
477
|
+
this.app.logger.warn(`[Orchestrator] Could not load settings: ${err.message}`);
|
|
478
|
+
}
|
|
479
|
+
|
|
390
480
|
// Leader election runs on app nodes only. Worker-only pods still load the
|
|
391
481
|
// plugin for monitoring/package installation, but they must not become the
|
|
392
482
|
// Kubernetes orchestrator leader.
|
|
@@ -396,41 +486,41 @@ export class PluginClusterManagerServer extends Plugin {
|
|
|
396
486
|
disabledReason: workerOnlyNode ? 'Worker-only nodes do not run orchestrator write operations.' : '',
|
|
397
487
|
});
|
|
398
488
|
await this.leaderElection.init();
|
|
399
|
-
|
|
400
|
-
(this.app as any).on('afterStart', async () => {
|
|
401
|
-
if (this.leaderElection) {
|
|
402
|
-
await this.leaderElection.tryBecomeLeader();
|
|
403
|
-
}
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
(this.app as any).on('beforeStop', async () => {
|
|
407
|
-
if (this.leaderElection) {
|
|
408
|
-
await this.leaderElection.release();
|
|
409
|
-
}
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
* Connect (or reconnect) the orchestrator adapter based on settings.
|
|
415
|
-
* Can be called at startup or when user saves new settings via UI.
|
|
416
|
-
*/
|
|
489
|
+
|
|
490
|
+
(this.app as any).on('afterStart', async () => {
|
|
491
|
+
if (this.leaderElection) {
|
|
492
|
+
await this.leaderElection.tryBecomeLeader();
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
(this.app as any).on('beforeStop', async () => {
|
|
497
|
+
if (this.leaderElection) {
|
|
498
|
+
await this.leaderElection.release();
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Connect (or reconnect) the orchestrator adapter based on settings.
|
|
505
|
+
* Can be called at startup or when user saves new settings via UI.
|
|
506
|
+
*/
|
|
417
507
|
public async connectAdapter(settings: any): Promise<boolean> {
|
|
418
|
-
const adapterType = settings?.adapterType || 'none';
|
|
419
|
-
|
|
420
|
-
if (adapterType === 'none') {
|
|
421
|
-
this.orchestrator = null;
|
|
422
|
-
this.app.logger.info('[Orchestrator] Adapter disabled');
|
|
423
|
-
return false;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
try {
|
|
427
|
-
if (adapterType === 'docker') {
|
|
428
|
-
const opts: any = {};
|
|
508
|
+
const adapterType = settings?.adapterType || 'none';
|
|
509
|
+
|
|
510
|
+
if (adapterType === 'none') {
|
|
511
|
+
this.orchestrator = null;
|
|
512
|
+
this.app.logger.info('[Orchestrator] Adapter disabled');
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
try {
|
|
517
|
+
if (adapterType === 'docker') {
|
|
518
|
+
const opts: any = {};
|
|
429
519
|
if (settings.dockerHost) {
|
|
430
|
-
// TCP connection (remote Docker or Docker Desktop on Windows)
|
|
431
|
-
const url = new URL(settings.dockerHost);
|
|
432
|
-
opts.host = url.hostname;
|
|
433
|
-
opts.port = parseInt(url.port, 10) || 2376;
|
|
520
|
+
// TCP connection (remote Docker or Docker Desktop on Windows)
|
|
521
|
+
const url = new URL(settings.dockerHost);
|
|
522
|
+
opts.host = url.hostname;
|
|
523
|
+
opts.port = parseInt(url.port, 10) || 2376;
|
|
434
524
|
} else {
|
|
435
525
|
opts.socketPath = settings.dockerSocketPath || '/var/run/docker.sock';
|
|
436
526
|
}
|
|
@@ -443,28 +533,28 @@ export class PluginClusterManagerServer extends Plugin {
|
|
|
443
533
|
namespace: settings.k8sNamespace || 'nocobase',
|
|
444
534
|
workerLabelSelector: settings.workerLabelSelector || 'role=worker',
|
|
445
535
|
});
|
|
446
|
-
this.app.logger.info('[Orchestrator] Kubernetes adapter initialized');
|
|
447
|
-
} else {
|
|
448
|
-
this.app.logger.warn(`[Orchestrator] Unknown adapter type: ${adapterType}`);
|
|
449
|
-
this.orchestrator = null;
|
|
450
|
-
return false;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// Test connectivity
|
|
454
|
-
const connected = await this.orchestrator.ping();
|
|
455
|
-
if (!connected) {
|
|
456
|
-
this.app.logger.error(`[Orchestrator] Failed to connect to ${adapterType} runtime`);
|
|
457
|
-
this.orchestrator = null;
|
|
458
|
-
return false;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
this.app.logger.info(`[Orchestrator] ✅ Connected to ${adapterType} runtime`);
|
|
462
|
-
return true;
|
|
463
|
-
} catch (err: any) {
|
|
464
|
-
this.app.logger.error(`[Orchestrator] Adapter init failed: ${err.message}`);
|
|
465
|
-
this.orchestrator = null;
|
|
466
|
-
return false;
|
|
467
|
-
}
|
|
536
|
+
this.app.logger.info('[Orchestrator] Kubernetes adapter initialized');
|
|
537
|
+
} else {
|
|
538
|
+
this.app.logger.warn(`[Orchestrator] Unknown adapter type: ${adapterType}`);
|
|
539
|
+
this.orchestrator = null;
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Test connectivity
|
|
544
|
+
const connected = await this.orchestrator.ping();
|
|
545
|
+
if (!connected) {
|
|
546
|
+
this.app.logger.error(`[Orchestrator] Failed to connect to ${adapterType} runtime`);
|
|
547
|
+
this.orchestrator = null;
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
this.app.logger.info(`[Orchestrator] ✅ Connected to ${adapterType} runtime`);
|
|
552
|
+
return true;
|
|
553
|
+
} catch (err: any) {
|
|
554
|
+
this.app.logger.error(`[Orchestrator] Adapter init failed: ${err.message}`);
|
|
555
|
+
this.orchestrator = null;
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
468
558
|
}
|
|
469
559
|
|
|
470
560
|
private isWorkerOnlyNode(): boolean {
|
|
@@ -472,5 +562,5 @@ export class PluginClusterManagerServer extends Plugin {
|
|
|
472
562
|
return workerMode === 'worker' || workerMode === 'task' || workerMode === '*';
|
|
473
563
|
}
|
|
474
564
|
}
|
|
475
|
-
|
|
476
|
-
export default PluginClusterManagerServer;
|
|
565
|
+
|
|
566
|
+
export default PluginClusterManagerServer;
|