plugin-cluster-manager 1.1.6 → 1.1.10

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 (65) hide show
  1. package/dist/client/AclCacheManager.d.ts +2 -0
  2. package/dist/client/CacheMonitor.d.ts +2 -0
  3. package/dist/client/ClusterManagerLayout.d.ts +2 -0
  4. package/dist/client/ClusterNodes.d.ts +2 -0
  5. package/dist/client/ContainerOrchestrator.d.ts +2 -0
  6. package/dist/client/EventQueueMonitor.d.ts +2 -0
  7. package/dist/client/LockMonitor.d.ts +2 -0
  8. package/dist/client/PackageInstaller.d.ts +2 -0
  9. package/dist/client/PluginOperations.d.ts +2 -0
  10. package/dist/client/RedisMonitor.d.ts +2 -0
  11. package/dist/client/TaskManager.d.ts +2 -0
  12. package/dist/client/WorkflowExecutions.d.ts +2 -0
  13. package/dist/client/index.d.ts +5 -0
  14. package/dist/client/index.js +1 -1
  15. package/dist/client/utils.d.ts +12 -0
  16. package/dist/externalVersion.js +5 -5
  17. package/dist/index.d.ts +2 -0
  18. package/dist/server/actions/acl-cache.d.ts +53 -0
  19. package/dist/server/actions/acl-cache.js +1 -1
  20. package/dist/server/actions/cache-monitor.d.ts +23 -0
  21. package/dist/server/actions/cluster-nodes.d.ts +49 -0
  22. package/dist/server/actions/event-queue-monitor.d.ts +13 -0
  23. package/dist/server/actions/lock-monitor.d.ts +19 -0
  24. package/dist/server/actions/orchestrator.d.ts +58 -0
  25. package/dist/server/actions/package-manager.d.ts +6 -0
  26. package/dist/server/actions/plugin-operations.d.ts +6 -0
  27. package/dist/server/actions/redis-monitor.d.ts +12 -0
  28. package/dist/server/actions/tasks.d.ts +7 -0
  29. package/dist/server/actions/workflow-executions.d.ts +7 -0
  30. package/dist/server/adapters/redis-lock-adapter.d.ts +15 -0
  31. package/dist/server/adapters/redis-node-registry.d.ts +12 -0
  32. package/dist/server/adapters/redis-pubsub-adapter.d.ts +16 -0
  33. package/dist/server/collections/app.d.ts +8 -0
  34. package/dist/server/collections/cluster-manager-acl-cache.d.ts +22 -0
  35. package/dist/server/collections/cluster-manager-cache-mgr.d.ts +22 -0
  36. package/dist/server/collections/cluster-manager-cluster.d.ts +22 -0
  37. package/dist/server/collections/cluster-manager-lock.d.ts +22 -0
  38. package/dist/server/collections/cluster-manager-plugins.d.ts +18 -0
  39. package/dist/server/collections/cluster-manager-queue.d.ts +22 -0
  40. package/dist/server/collections/cluster-manager-redis.d.ts +22 -0
  41. package/dist/server/collections/cluster-manager-workflow.d.ts +22 -0
  42. package/dist/server/collections/cluster-manager.d.ts +22 -0
  43. package/dist/server/collections/orchestrator-settings.d.ts +59 -0
  44. package/dist/server/collections/orchestrator-stacks.d.ts +102 -0
  45. package/dist/server/collections/worker-orchestrator.d.ts +22 -0
  46. package/dist/server/collections/worker-packages-configs.d.ts +3 -0
  47. package/dist/server/collections/worker-packages.d.ts +22 -0
  48. package/dist/server/index.d.ts +1 -0
  49. package/dist/server/orchestrator/PackageManager.d.ts +39 -0
  50. package/dist/server/orchestrator/PackageManager.js +63 -11
  51. package/dist/server/orchestrator/docker-adapter.d.ts +41 -0
  52. package/dist/server/orchestrator/index.d.ts +4 -0
  53. package/dist/server/orchestrator/k8s-adapter.d.ts +50 -0
  54. package/dist/server/orchestrator/leader-election.d.ts +48 -0
  55. package/dist/server/orchestrator/types.d.ts +84 -0
  56. package/dist/server/plugin.d.ts +26 -0
  57. package/dist/server/plugin.js +9 -0
  58. package/dist/server/utils/node.d.ts +6 -0
  59. package/dist/server/utils/redis.d.ts +29 -0
  60. package/dist/shared/packages.d.ts +23 -0
  61. package/package.json +1 -1
  62. package/src/client/PluginOperations.tsx +13 -5
  63. package/src/server/actions/acl-cache.ts +2 -2
  64. package/src/server/orchestrator/PackageManager.ts +84 -17
  65. package/src/server/plugin.ts +15 -0
@@ -32,7 +32,14 @@ export function PluginOperations() {
32
32
  setLoading(true);
33
33
  try {
34
34
  const res = await api.request({ url: 'clusterManagerPlugins:list' });
35
- setPlugins(res?.data?.data || []);
35
+ const data = Array.isArray(res?.data?.data?.data)
36
+ ? res.data.data.data
37
+ : Array.isArray(res?.data?.data)
38
+ ? res.data.data
39
+ : Array.isArray(res?.data)
40
+ ? res.data
41
+ : [];
42
+ setPlugins(data);
36
43
  } catch (err: any) {
37
44
  message.error(getErrorMessage(err, t('Failed to load plugins')));
38
45
  } finally {
@@ -46,8 +53,9 @@ export function PluginOperations() {
46
53
 
47
54
  const filteredPlugins = useMemo(() => {
48
55
  const keyword = search.trim().toLowerCase();
49
- if (!keyword) return plugins;
50
- return plugins.filter((plugin) =>
56
+ const list = Array.isArray(plugins) ? plugins : [];
57
+ if (!keyword) return list;
58
+ return list.filter((plugin) =>
51
59
  [plugin.name, plugin.packageName, plugin.displayName, plugin.description]
52
60
  .filter(Boolean)
53
61
  .some((value) => String(value).toLowerCase().includes(keyword)),
@@ -63,7 +71,7 @@ export function PluginOperations() {
63
71
  method: 'post',
64
72
  data: { name: record.name },
65
73
  });
66
- message.success(res?.data?.message || t('Plugin force disabled'));
74
+ message.success(res?.data?.data?.message || res?.data?.message || t('Plugin force disabled'));
67
75
  await fetchData();
68
76
  } catch (err: any) {
69
77
  message.error(getErrorMessage(err, t('Failed to force disable plugin')));
@@ -81,7 +89,7 @@ export function PluginOperations() {
81
89
  method: 'post',
82
90
  data: { name: record.name },
83
91
  });
84
- message.success(res?.data?.message || t('Plugin force removed'));
92
+ message.success(res?.data?.data?.message || res?.data?.message || t('Plugin force removed'));
85
93
  await fetchData();
86
94
  } catch (err: any) {
87
95
  message.error(getErrorMessage(err, t('Failed to force remove plugin')));
@@ -108,8 +108,8 @@ export function createAclCacheMiddleware(app: any) {
108
108
  // This is safe because we read ctx.permission AFTER next() completes —
109
109
  // no monkey-patching of shared singletons.
110
110
  try {
111
- const result = ctx.permission?.can;
112
- const valueToCache = result ? JSON.stringify(result) : '__DENIED__';
111
+ const result = ctx.permission?.can as any;
112
+ const valueToCache = JSON.stringify(result !== undefined && result !== null ? result : true);
113
113
  cache.set(cacheKey, valueToCache, ACL_CACHE_TTL).catch(() => {});
114
114
  } catch {
115
115
  // Ignore cache write errors
@@ -5,7 +5,7 @@ import path from 'path';
5
5
  import Application from '@nocobase/server';
6
6
 
7
7
  /** Allow only safe package name characters: letters, digits, dash, underscore, dot, @, /, [, ] */
8
- const SAFE_PKG_RE = /^[a-zA-Z0-9_\-\.@\/\[\]]+$/;
8
+ const SAFE_PKG_RE = /^(?:[a-zA-Z0-9_.@/-]|\[|\])+$/;
9
9
  const INSTALL_CHANNEL = 'cluster-manager.install-packages';
10
10
 
11
11
  type TargetRole = 'app' | 'worker' | 'sandbox' | 'all';
@@ -17,6 +17,11 @@ interface InstallPayload {
17
17
  registryConfig?: { aptMirrorUrl?: string; npmRegistryUrl?: string; pypiIndexUrl?: string; pypiTrustedHost?: string };
18
18
  }
19
19
 
20
+ interface AptOsInfo {
21
+ id: string;
22
+ codename: string;
23
+ }
24
+
20
25
  function sanitizePkg(name: string): string {
21
26
  if (typeof name !== 'string') {
22
27
  throw new Error('Package name must be a string');
@@ -81,10 +86,44 @@ function formatCommand(command: string, args: string[]): string {
81
86
  .join(' ');
82
87
  }
83
88
 
89
+ function parseOsRelease(content: string): Record<string, string> {
90
+ const values: Record<string, string> = {};
91
+ for (const line of content.split('\n')) {
92
+ const match = line.match(/^([A-Z0-9_]+)=(.*)$/);
93
+ if (!match) continue;
94
+
95
+ values[match[1]] = match[2].replace(/^"|"$/g, '');
96
+ }
97
+ return values;
98
+ }
99
+
100
+ function normalizeMirrorUrl(value: string): string {
101
+ return value.endsWith('/') ? value : `${value}/`;
102
+ }
103
+
104
+ function isInternalCacheMirror(url: URL): boolean {
105
+ return url.hostname === 'nginx-cache-registry';
106
+ }
107
+
108
+ function resolveAptMirrorForOs(aptMirrorUrl: string, osInfo: AptOsInfo, logs: string[]): string {
109
+ const url = new URL(normalizeMirrorUrl(aptMirrorUrl));
110
+ const original = url.toString();
111
+
112
+ if (isInternalCacheMirror(url) && osInfo.id === 'debian' && url.pathname === '/ubuntu/') {
113
+ url.pathname = '/debian/';
114
+ } else if (isInternalCacheMirror(url) && osInfo.id === 'ubuntu' && url.pathname === '/debian/') {
115
+ url.pathname = '/ubuntu/';
116
+ }
117
+
118
+ const resolved = url.toString();
119
+ if (resolved !== original) {
120
+ logs.push(`Adjusted APT mirror for ${osInfo.id}: ${redactUrl(resolved)}`);
121
+ }
122
+ return resolved;
123
+ }
124
+
84
125
  export class PackageManager {
85
- constructor(
86
- private app: Application,
87
- ) {}
126
+ constructor(private app: Application) {}
88
127
 
89
128
  /**
90
129
  * Called from REST action when admin clicks "Install Packages".
@@ -107,9 +146,7 @@ export class PackageManager {
107
146
  // Filter by role
108
147
  const currentRole = getCurrentRole();
109
148
  const roleMatches =
110
- targetRole === 'all' ||
111
- targetRole === currentRole ||
112
- (targetRole === 'worker' && currentRole === 'sandbox');
149
+ targetRole === 'all' || targetRole === currentRole || (targetRole === 'worker' && currentRole === 'sandbox');
113
150
 
114
151
  if (!roleMatches) {
115
152
  return; // Skip if role doesn't match
@@ -138,7 +175,7 @@ export class PackageManager {
138
175
  if (registryConfig.aptMirrorUrl) {
139
176
  const aptMirrorUrl = sanitizeHttpUrl(registryConfig.aptMirrorUrl, 'APT mirror URL');
140
177
  logs.push(`Applying APT mirror: ${redactUrl(aptMirrorUrl)}`);
141
- await this.configureAptMirror(aptMirrorUrl);
178
+ await this.configureAptMirror(aptMirrorUrl, logs);
142
179
  }
143
180
 
144
181
  await this.updateInstallStatus('running', 20, 'Installing APT packages...', logs);
@@ -241,7 +278,13 @@ export class PackageManager {
241
278
  /**
242
279
  * Run a command without a shell so registry URLs and package names are not re-parsed as shell syntax.
243
280
  */
244
- private async runCommand(command: string, args: string[], label: string, logs: string[], timeoutMs = 1200000): Promise<void> {
281
+ private async runCommand(
282
+ command: string,
283
+ args: string[],
284
+ label: string,
285
+ logs: string[],
286
+ timeoutMs = 1200000,
287
+ ): Promise<void> {
245
288
  logs.push(`RUNNING: ${formatCommand(command, args)}`);
246
289
  logs.push(`${label}`);
247
290
 
@@ -250,12 +293,12 @@ export class PackageManager {
250
293
  let stdout = '';
251
294
  let stderr = '';
252
295
  let settled = false;
253
- let timer: NodeJS.Timeout;
296
+ const state: { timer?: NodeJS.Timeout } = {};
254
297
 
255
298
  const finish = (error?: Error) => {
256
299
  if (settled) return;
257
300
  settled = true;
258
- clearTimeout(timer);
301
+ if (state.timer) clearTimeout(state.timer);
259
302
  if (stdout) logs.push(stdout.slice(0, 500));
260
303
  if (stderr) logs.push(`WARN: ${stderr.slice(0, 300)}`);
261
304
  if (error) {
@@ -266,7 +309,7 @@ export class PackageManager {
266
309
  }
267
310
  };
268
311
 
269
- timer = setTimeout(() => {
312
+ state.timer = setTimeout(() => {
270
313
  child.kill('SIGTERM');
271
314
  finish(new Error(`${command} timed out after ${timeoutMs}ms`));
272
315
  }, timeoutMs);
@@ -324,16 +367,16 @@ export class PackageManager {
324
367
  const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'ignore'] });
325
368
  let stdout = '';
326
369
  let settled = false;
327
- let timer: NodeJS.Timeout;
370
+ const state: { timer?: NodeJS.Timeout } = {};
328
371
 
329
372
  const finish = (ok: boolean) => {
330
373
  if (settled) return;
331
374
  settled = true;
332
- clearTimeout(timer);
375
+ if (state.timer) clearTimeout(state.timer);
333
376
  resolve(ok);
334
377
  };
335
378
 
336
- timer = setTimeout(() => {
379
+ state.timer = setTimeout(() => {
337
380
  child.kill('SIGTERM');
338
381
  finish(false);
339
382
  }, timeoutMs);
@@ -346,7 +389,31 @@ export class PackageManager {
346
389
  });
347
390
  }
348
391
 
349
- private async configureAptMirror(aptMirrorUrl: string): Promise<void> {
392
+ private async getAptOsInfo(): Promise<AptOsInfo> {
393
+ const values = parseOsRelease(await fsp.readFile('/etc/os-release', 'utf8'));
394
+ const id = (values.ID || '').toLowerCase();
395
+ const codename = values.VERSION_CODENAME || values.UBUNTU_CODENAME;
396
+
397
+ if (!id || !codename) {
398
+ throw new Error('Cannot detect OS ID/version codename from /etc/os-release for APT mirror configuration.');
399
+ }
400
+ return { id, codename };
401
+ }
402
+
403
+ private buildAptSources(aptMirrorUrl: string, osInfo: AptOsInfo): string {
404
+ const mirror = normalizeMirrorUrl(aptMirrorUrl);
405
+ if (osInfo.id === 'ubuntu') {
406
+ return `deb ${mirror} ${osInfo.codename} main universe restricted multiverse\n`;
407
+ }
408
+ if (osInfo.id === 'debian') {
409
+ return `deb ${mirror} ${osInfo.codename} main contrib non-free non-free-firmware\n`;
410
+ }
411
+ return `deb ${mirror} ${osInfo.codename} main\n`;
412
+ }
413
+
414
+ private async configureAptMirror(aptMirrorUrl: string, logs: string[]): Promise<void> {
415
+ const osInfo = await this.getAptOsInfo();
416
+ const resolvedMirrorUrl = resolveAptMirrorForOs(aptMirrorUrl, osInfo, logs);
350
417
  const backupDir = '/etc/apt/sources.list.d.bak';
351
418
  await fsp.mkdir(backupDir, { recursive: true });
352
419
 
@@ -362,7 +429,7 @@ export class PackageManager {
362
429
  // sources.list.d may not exist in minimal images.
363
430
  }
364
431
 
365
- await fsp.writeFile('/etc/apt/sources.list', `deb ${aptMirrorUrl} bookworm main\n`, 'utf8');
432
+ await fsp.writeFile('/etc/apt/sources.list', this.buildAptSources(resolvedMirrorUrl, osInfo), 'utf8');
366
433
  }
367
434
 
368
435
  private async moveIfExists(from: string, to: string): Promise<void> {
@@ -33,6 +33,21 @@ export class PluginClusterManagerServer extends Plugin {
33
33
  }
34
34
 
35
35
  async load() {
36
+ // Fix NocoBase core strategy resource permission check crash:
37
+ // Attachments collection has "createdBy: true" options but lacks explicit 'createdById' metadata field registration.
38
+ // When non-root roles upload attachments, core ACL merges 'own' filters (createdById) and calls checkFilterParams,
39
+ // which throws a NoPermissionError because getField('createdById') returns undefined.
40
+ // Registering 'createdById' explicitly as a metadata field on the attachments collection prevents this check from crashing.
41
+ this.db.extendCollection({
42
+ name: 'attachments',
43
+ fields: [
44
+ {
45
+ type: 'bigInt',
46
+ name: 'createdById',
47
+ },
48
+ ],
49
+ });
50
+
36
51
  this.nodeRegistry = new RedisNodeRegistry(this.app);
37
52
 
38
53
  (this.app as any).on('afterStart', () => {