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.
- 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/EventQueueMonitor.d.ts +2 -0
- package/dist/client/LockMonitor.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.d.ts +12 -0
- package/dist/externalVersion.js +5 -5
- package/dist/index.d.ts +2 -0
- 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 +23 -0
- package/dist/server/actions/cluster-nodes.d.ts +49 -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-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/index.d.ts +1 -0
- package/dist/server/orchestrator/PackageManager.d.ts +39 -0
- package/dist/server/orchestrator/PackageManager.js +63 -11
- 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 +9 -0
- package/dist/server/utils/node.d.ts +6 -0
- package/dist/server/utils/redis.d.ts +29 -0
- package/dist/shared/packages.d.ts +23 -0
- package/package.json +1 -1
- package/src/client/PluginOperations.tsx +13 -5
- package/src/server/actions/acl-cache.ts +2 -2
- package/src/server/orchestrator/PackageManager.ts +84 -17
- 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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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',
|
|
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> {
|
package/src/server/plugin.ts
CHANGED
|
@@ -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', () => {
|