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.
Files changed (108) hide show
  1. package/client.js +1 -0
  2. package/dist/client/AclCacheManager.d.ts +2 -0
  3. package/dist/client/CacheMonitor.d.ts +2 -0
  4. package/dist/client/ClusterManagerLayout.d.ts +2 -0
  5. package/dist/client/ClusterNodes.d.ts +2 -0
  6. package/dist/client/ContainerOrchestrator.d.ts +2 -0
  7. package/dist/client/Doctor.d.ts +2 -0
  8. package/dist/client/EventQueueMonitor.d.ts +2 -0
  9. package/dist/client/LockMonitor.d.ts +2 -0
  10. package/dist/client/NginxCacheManager.d.ts +2 -0
  11. package/dist/client/PackageInstaller.d.ts +2 -0
  12. package/dist/client/PluginOperations.d.ts +2 -0
  13. package/dist/client/RedisMonitor.d.ts +2 -0
  14. package/dist/client/TaskManager.d.ts +2 -0
  15. package/dist/client/WorkflowExecutions.d.ts +2 -0
  16. package/dist/client/index.d.ts +5 -0
  17. package/dist/client/index.js +1 -1
  18. package/dist/client/utils/clientSafeCache.d.ts +3 -0
  19. package/dist/client/utils/requestDedupInterceptor.d.ts +2 -0
  20. package/dist/client/utils.d.ts +12 -0
  21. package/dist/externalVersion.js +5 -5
  22. package/dist/index.d.ts +2 -0
  23. package/dist/locale/en-US.json +97 -1
  24. package/dist/locale/vi-VN.json +98 -1
  25. package/dist/locale/zh-CN.json +98 -1
  26. package/dist/server/actions/acl-cache.d.ts +53 -0
  27. package/dist/server/actions/acl-cache.js +1 -1
  28. package/dist/server/actions/cache-monitor.d.ts +33 -0
  29. package/dist/server/actions/cache-monitor.js +301 -0
  30. package/dist/server/actions/cluster-nodes.d.ts +64 -0
  31. package/dist/server/actions/cluster-nodes.js +394 -10
  32. package/dist/server/actions/doctor.d.ts +82 -0
  33. package/dist/server/actions/doctor.js +1250 -0
  34. package/dist/server/actions/event-queue-monitor.d.ts +13 -0
  35. package/dist/server/actions/lock-monitor.d.ts +19 -0
  36. package/dist/server/actions/orchestrator.d.ts +58 -0
  37. package/dist/server/actions/package-manager.d.ts +6 -0
  38. package/dist/server/actions/plugin-operations.d.ts +6 -0
  39. package/dist/server/actions/redis-monitor.d.ts +12 -0
  40. package/dist/server/actions/tasks.d.ts +7 -0
  41. package/dist/server/actions/workflow-executions.d.ts +7 -0
  42. package/dist/server/adapters/redis-lock-adapter.d.ts +15 -0
  43. package/dist/server/adapters/redis-node-registry.d.ts +12 -0
  44. package/dist/server/adapters/redis-pubsub-adapter.d.ts +16 -0
  45. package/dist/server/collections/app.d.ts +8 -0
  46. package/dist/server/collections/cluster-manager-acl-cache.d.ts +22 -0
  47. package/dist/server/collections/cluster-manager-cache-mgr.d.ts +22 -0
  48. package/dist/server/collections/cluster-manager-cluster.d.ts +22 -0
  49. package/dist/server/collections/cluster-manager-doctor-runs.d.ts +3 -0
  50. package/dist/server/collections/cluster-manager-doctor-runs.js +52 -0
  51. package/dist/server/collections/cluster-manager-doctor.d.ts +18 -0
  52. package/dist/server/collections/cluster-manager-doctor.js +44 -0
  53. package/dist/server/collections/cluster-manager-lock.d.ts +22 -0
  54. package/dist/server/collections/cluster-manager-plugins.d.ts +18 -0
  55. package/dist/server/collections/cluster-manager-queue.d.ts +22 -0
  56. package/dist/server/collections/cluster-manager-redis.d.ts +22 -0
  57. package/dist/server/collections/cluster-manager-workflow.d.ts +22 -0
  58. package/dist/server/collections/cluster-manager.d.ts +22 -0
  59. package/dist/server/collections/orchestrator-settings.d.ts +59 -0
  60. package/dist/server/collections/orchestrator-stacks.d.ts +102 -0
  61. package/dist/server/collections/worker-orchestrator.d.ts +22 -0
  62. package/dist/server/collections/worker-packages-configs.d.ts +3 -0
  63. package/dist/server/collections/worker-packages.d.ts +22 -0
  64. package/dist/server/hooks/cacheInvalidationHooks.d.ts +1 -0
  65. package/dist/server/hooks/cacheInvalidationHooks.js +81 -0
  66. package/dist/server/index.d.ts +1 -0
  67. package/dist/server/middlewares/listMetaCacheMiddleware.d.ts +2 -0
  68. package/dist/server/middlewares/listMetaCacheMiddleware.js +79 -0
  69. package/dist/server/orchestrator/PackageManager.d.ts +39 -0
  70. package/dist/server/orchestrator/PackageManager.js +83 -27
  71. package/dist/server/orchestrator/docker-adapter.d.ts +41 -0
  72. package/dist/server/orchestrator/index.d.ts +4 -0
  73. package/dist/server/orchestrator/k8s-adapter.d.ts +50 -0
  74. package/dist/server/orchestrator/leader-election.d.ts +48 -0
  75. package/dist/server/orchestrator/types.d.ts +84 -0
  76. package/dist/server/plugin.d.ts +26 -0
  77. package/dist/server/plugin.js +70 -8
  78. package/dist/server/utils/node.d.ts +6 -0
  79. package/dist/server/utils/redis.d.ts +29 -0
  80. package/dist/server/utils/versionManager.d.ts +10 -0
  81. package/dist/server/utils/versionManager.js +91 -0
  82. package/dist/shared/packages.d.ts +23 -0
  83. package/package.json +41 -41
  84. package/server.js +1 -0
  85. package/src/client/CacheMonitor.tsx +166 -179
  86. package/src/client/ClusterManagerLayout.tsx +48 -42
  87. package/src/client/ClusterNodes.tsx +691 -418
  88. package/src/client/Doctor.tsx +559 -0
  89. package/src/client/NginxCacheManager.tsx +415 -0
  90. package/src/client/PluginOperations.tsx +234 -234
  91. package/src/client/index.tsx +22 -14
  92. package/src/client/utils/clientSafeCache.ts +41 -0
  93. package/src/client/utils/requestDedupInterceptor.ts +213 -0
  94. package/src/locale/en-US.json +97 -1
  95. package/src/locale/vi-VN.json +98 -1
  96. package/src/locale/zh-CN.json +98 -1
  97. package/src/server/__tests__/doctor.test.ts +53 -0
  98. package/src/server/actions/acl-cache.ts +272 -272
  99. package/src/server/actions/cache-monitor.ts +453 -116
  100. package/src/server/actions/cluster-nodes.ts +882 -378
  101. package/src/server/actions/doctor.ts +1540 -0
  102. package/src/server/collections/cluster-manager-doctor-runs.ts +23 -0
  103. package/src/server/collections/cluster-manager-doctor.ts +19 -0
  104. package/src/server/hooks/cacheInvalidationHooks.ts +58 -0
  105. package/src/server/middlewares/listMetaCacheMiddleware.ts +55 -0
  106. package/src/server/orchestrator/PackageManager.ts +19 -15
  107. package/src/server/plugin.ts +353 -263
  108. package/src/server/utils/versionManager.ts +69 -0
@@ -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 = mode === 'worker' || mode === 'task' || mode === '*' || process.env.APP_ROLE === 'worker' || process.env.APP_ROLE === 'sandbox';
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
- if (msg.startsWith('{')) {
170
- const parsed = JSON.parse(msg);
171
- target = parsed.hostname;
172
- mode = parsed.mode || 'hard';
173
- }
174
-
175
- if (target === os.hostname() || target === '*') {
176
- this.app.logger.warn(`[ClusterManager] Received ${mode} restart command for node ${os.hostname()}...`);
177
- setTimeout(async () => {
178
- try {
179
- if (mode === 'soft') {
180
- this.app.logger.warn(`[ClusterManager] Triggering NocoBase Soft Restart...`);
181
- await this.app.restart();
182
- } else {
183
- this.app.logger.warn(`[ClusterManager] Shutting down Node.js process for Hard Restart...`);
184
- await this.app.stop();
185
- process.exit(1);
186
- }
187
- } catch (e: any) {}
188
- }, 1000); // 1-second delay so HTTP API can gracefully respond first
189
- }
190
- } catch (err) {
191
- this.app.logger.error(`[ClusterManager] Parse error for restart message: ${msg}`);
192
- }
193
- });
194
- }
195
-
196
- // Task management (reads asyncTasks table)
197
- this.app.resourcer.define({
198
- name: 'clusterManager',
199
- actions: tasksActions,
200
- });
201
-
202
- // Workflow execution management (reads executions + jobs tables)
203
- this.app.resourcer.define({
204
- name: 'clusterManagerWorkflow',
205
- actions: workflowActions,
206
- });
207
-
208
- // Redis live metrics
209
- this.app.resourcer.define({
210
- name: 'clusterManagerRedis',
211
- actions: redisActions,
212
- });
213
-
214
- // ACL cache management
215
- this.app.resourcer.define({
216
- name: 'clusterManagerAclCache',
217
- actions: aclCacheActions,
218
- });
219
-
220
- // Cluster nodes & health
221
- this.app.resourcer.define({
222
- name: 'clusterManagerCluster',
223
- actions: clusterActions,
224
- });
225
-
226
- // Event queue monitoring
227
- this.app.resourcer.define({
228
- name: 'clusterManagerQueue',
229
- actions: eventQueueActions,
230
- });
231
-
232
- // Distributed lock monitoring
233
- this.app.resourcer.define({
234
- name: 'clusterManagerLock',
235
- actions: lockActions,
236
- });
237
-
238
- // Cache manager monitoring
239
- this.app.resourcer.define({
240
- name: 'clusterManagerCacheMgr',
241
- actions: cacheMonitorActions,
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
- // Lightweight healthcheck endpoint avoiding workflow pre-action and resourcer spam
265
- this.app.use(async (ctx: any, next: any) => {
266
- if (ctx.path === '/api/clusterManager:health' && (ctx.method === 'GET' || ctx.method === 'HEAD')) {
267
- ctx.body = {
268
- status: 'ok',
269
- version: process.env.NOCOBASE_VERSION || process.version,
270
- mode: process.env.WORKER_MODE || 'main',
271
- };
272
- return;
273
- }
274
- await next();
275
- });
276
-
277
- // Admin-only access
278
- this.app.acl.registerSnippet({
279
- name: `pm.${this.name}`,
280
- actions: [
281
- 'clusterManager:*',
282
- 'clusterManagerWorkflow:*',
283
- 'clusterManagerRedis:*',
284
- 'clusterManagerAclCache:*',
285
- 'clusterManagerCluster:*',
286
- 'clusterManagerQueue:*',
287
- 'clusterManagerLock:*',
288
- 'clusterManagerCacheMgr:*',
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;