plugin-cluster-manager 1.1.5 → 1.1.6
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/README.md +17 -5
- package/dist/client/index.js +1 -1
- package/dist/externalVersion.js +5 -5
- package/dist/locale/en-US.json +28 -4
- package/dist/locale/vi-VN.json +28 -4
- package/dist/locale/zh-CN.json +28 -4
- package/dist/server/actions/event-queue-monitor.js +123 -1
- package/dist/server/actions/orchestrator.js +1 -1
- package/dist/server/actions/plugin-operations.js +171 -0
- package/dist/server/collections/cluster-manager-plugins.js +44 -0
- package/dist/server/plugin.js +8 -2
- package/package.json +9 -4
- package/src/client/ClusterManagerLayout.tsx +16 -10
- package/src/client/EventQueueMonitor.tsx +349 -202
- package/src/client/PluginOperations.tsx +226 -0
- package/src/locale/en-US.json +28 -4
- package/src/locale/vi-VN.json +28 -4
- package/src/locale/zh-CN.json +28 -4
- package/src/server/actions/event-queue-monitor.ts +234 -95
- package/src/server/actions/orchestrator.ts +1 -1
- package/src/server/actions/plugin-operations.ts +151 -0
- package/src/server/collections/cluster-manager-plugins.ts +19 -0
- package/src/server/plugin.ts +28 -20
- package/dist/client/AclCacheManager.d.ts +0 -2
- package/dist/client/CacheMonitor.d.ts +0 -2
- package/dist/client/ClusterManagerLayout.d.ts +0 -2
- package/dist/client/ClusterNodes.d.ts +0 -2
- package/dist/client/ContainerOrchestrator.d.ts +0 -2
- package/dist/client/EventQueueMonitor.d.ts +0 -2
- package/dist/client/LockMonitor.d.ts +0 -2
- package/dist/client/PackageInstaller.d.ts +0 -2
- package/dist/client/RedisMonitor.d.ts +0 -2
- package/dist/client/TaskManager.d.ts +0 -2
- package/dist/client/WorkflowExecutions.d.ts +0 -2
- package/dist/client/index.d.ts +0 -5
- package/dist/client/utils.d.ts +0 -12
- package/dist/index.d.ts +0 -2
- package/dist/server/actions/acl-cache.d.ts +0 -53
- package/dist/server/actions/cache-monitor.d.ts +0 -23
- package/dist/server/actions/cluster-nodes.d.ts +0 -49
- package/dist/server/actions/event-queue-monitor.d.ts +0 -13
- package/dist/server/actions/lock-monitor.d.ts +0 -19
- package/dist/server/actions/orchestrator.d.ts +0 -58
- package/dist/server/actions/package-manager.d.ts +0 -6
- package/dist/server/actions/redis-monitor.d.ts +0 -12
- package/dist/server/actions/tasks.d.ts +0 -7
- package/dist/server/actions/workflow-executions.d.ts +0 -7
- package/dist/server/adapters/redis-lock-adapter.d.ts +0 -15
- package/dist/server/adapters/redis-node-registry.d.ts +0 -12
- package/dist/server/adapters/redis-pubsub-adapter.d.ts +0 -16
- package/dist/server/collections/app.d.ts +0 -8
- package/dist/server/collections/cluster-manager-acl-cache.d.ts +0 -22
- package/dist/server/collections/cluster-manager-cache-mgr.d.ts +0 -22
- package/dist/server/collections/cluster-manager-cluster.d.ts +0 -22
- package/dist/server/collections/cluster-manager-lock.d.ts +0 -22
- package/dist/server/collections/cluster-manager-queue.d.ts +0 -22
- package/dist/server/collections/cluster-manager-redis.d.ts +0 -22
- package/dist/server/collections/cluster-manager-workflow.d.ts +0 -22
- package/dist/server/collections/cluster-manager.d.ts +0 -22
- package/dist/server/collections/orchestrator-settings.d.ts +0 -59
- package/dist/server/collections/orchestrator-stacks.d.ts +0 -102
- package/dist/server/collections/worker-orchestrator.d.ts +0 -22
- package/dist/server/collections/worker-packages-configs.d.ts +0 -3
- package/dist/server/collections/worker-packages.d.ts +0 -22
- package/dist/server/index.d.ts +0 -1
- package/dist/server/orchestrator/PackageManager.d.ts +0 -37
- package/dist/server/orchestrator/docker-adapter.d.ts +0 -41
- package/dist/server/orchestrator/index.d.ts +0 -4
- package/dist/server/orchestrator/k8s-adapter.d.ts +0 -50
- package/dist/server/orchestrator/leader-election.d.ts +0 -48
- package/dist/server/orchestrator/types.d.ts +0 -84
- package/dist/server/plugin.d.ts +0 -26
- package/dist/server/utils/node.d.ts +0 -6
- package/dist/server/utils/redis.d.ts +0 -29
- package/dist/shared/packages.d.ts +0 -23
|
@@ -1,95 +1,234 @@
|
|
|
1
|
-
import { Context } from '@nocobase/actions';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
1
|
+
import { Context } from '@nocobase/actions';
|
|
2
|
+
import { scanKeys } from '../utils/redis';
|
|
3
|
+
|
|
4
|
+
const REDIS_QUEUE_CONNECTION = 'cluster-manager:queue-monitor';
|
|
5
|
+
const REDIS_QUEUE_PATTERNS = ['*:plugin-git-manager:review:queue', '*:plugin-build-guide-block:build:queue'];
|
|
6
|
+
|
|
7
|
+
function getQueueRedisUrl() {
|
|
8
|
+
return process.env.QUEUE_ADAPTER_REDIS_URL || process.env.REDIS_URL;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function getQueueRedis(ctx: Context) {
|
|
12
|
+
const url = getQueueRedisUrl();
|
|
13
|
+
const manager = (ctx.app as any).redisConnectionManager;
|
|
14
|
+
if (!url || !manager?.getConnectionSync) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return manager.getConnectionSync(REDIS_QUEUE_CONNECTION, { connectionString: url });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function knownRedisQueueKeys(ctx: Context) {
|
|
21
|
+
const appName = (ctx.app as any).name || process.env.APP_NAME || 'main';
|
|
22
|
+
return [`${appName}:plugin-git-manager:review:queue`, `${appName}:plugin-build-guide-block:build:queue`];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isKnownRedisQueueKey(key: string) {
|
|
26
|
+
return REDIS_QUEUE_PATTERNS.some((pattern) => {
|
|
27
|
+
const suffix = pattern.replace('*:', '');
|
|
28
|
+
return key === suffix || key.endsWith(`:${suffix}`);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function describeRedisQueueKey(key: string) {
|
|
33
|
+
const parts = String(key).split(':');
|
|
34
|
+
const queue = parts[parts.length - 2] || key;
|
|
35
|
+
const plugin = parts[parts.length - 3] || 'unknown';
|
|
36
|
+
const appName = parts.slice(0, Math.max(1, parts.length - 3)).join(':') || 'main';
|
|
37
|
+
return {
|
|
38
|
+
appName,
|
|
39
|
+
plugin,
|
|
40
|
+
queue,
|
|
41
|
+
channel: `${plugin}.${queue}`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function getRedisQueues(ctx: Context) {
|
|
46
|
+
const redis = await getQueueRedis(ctx);
|
|
47
|
+
if (!redis) {
|
|
48
|
+
return {
|
|
49
|
+
connected: false,
|
|
50
|
+
urlConfigured: Boolean(getQueueRedisUrl()),
|
|
51
|
+
queues: [],
|
|
52
|
+
note: 'Redis queue connection is not configured',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const keys = new Set<string>(knownRedisQueueKeys(ctx));
|
|
57
|
+
for (const pattern of REDIS_QUEUE_PATTERNS) {
|
|
58
|
+
try {
|
|
59
|
+
const scanned = await scanKeys(redis, pattern, 200);
|
|
60
|
+
scanned.forEach((key) => keys.add(key));
|
|
61
|
+
} catch {
|
|
62
|
+
// Keep known keys even if SCAN is not available.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const queues = [];
|
|
67
|
+
for (const key of keys) {
|
|
68
|
+
if (!isKnownRedisQueueKey(key)) continue;
|
|
69
|
+
let pending = 0;
|
|
70
|
+
try {
|
|
71
|
+
pending = Number(await redis.sendCommand(['LLEN', key])) || 0;
|
|
72
|
+
} catch {
|
|
73
|
+
pending = 0;
|
|
74
|
+
}
|
|
75
|
+
queues.push({
|
|
76
|
+
source: 'redis',
|
|
77
|
+
key,
|
|
78
|
+
type: 'list',
|
|
79
|
+
pending,
|
|
80
|
+
...describeRedisQueueKey(key),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
connected: true,
|
|
86
|
+
urlConfigured: true,
|
|
87
|
+
queues,
|
|
88
|
+
totalPending: queues.reduce((sum, queue) => sum + (queue.pending || 0), 0),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseRedisQueueMessage(raw: string, key: string, index: number) {
|
|
93
|
+
let content: any = raw;
|
|
94
|
+
try {
|
|
95
|
+
content = JSON.parse(raw);
|
|
96
|
+
} catch {
|
|
97
|
+
// Keep the raw string for non-JSON messages.
|
|
98
|
+
}
|
|
99
|
+
const queuedAt = content?.queuedAt ? Date.parse(content.queuedAt) : null;
|
|
100
|
+
return {
|
|
101
|
+
id: `${key}:${index}`,
|
|
102
|
+
index,
|
|
103
|
+
content,
|
|
104
|
+
raw,
|
|
105
|
+
timestamp: Number.isFinite(queuedAt) ? queuedAt : null,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const eventQueueActions = {
|
|
110
|
+
/**
|
|
111
|
+
* GET /clusterManagerQueue:stats
|
|
112
|
+
* Returns event queue statistics
|
|
113
|
+
*/
|
|
114
|
+
async stats(ctx: Context, next: () => Promise<void>) {
|
|
115
|
+
const eq = ctx.app.eventQueue;
|
|
116
|
+
if (!eq) {
|
|
117
|
+
ctx.throw(503, 'Event queue is not available');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const adapter = (eq as any).adapter;
|
|
121
|
+
const events = (eq as any).events as Map<string, any>;
|
|
122
|
+
const adapterName = adapter?.constructor?.name || 'unknown';
|
|
123
|
+
const connected = eq.isConnected();
|
|
124
|
+
|
|
125
|
+
const channels: any[] = [];
|
|
126
|
+
if (events) {
|
|
127
|
+
for (const [channel, opts] of events.entries()) {
|
|
128
|
+
const queueData: any = { channel };
|
|
129
|
+
queueData.concurrency = opts.concurrency || 1;
|
|
130
|
+
queueData.interval = opts.interval || 250;
|
|
131
|
+
|
|
132
|
+
// For MemoryEventQueueAdapter, we can peek at queue depth
|
|
133
|
+
if (adapter?.queues) {
|
|
134
|
+
const fullChannel = eq.getFullChannel(channel, opts.shared);
|
|
135
|
+
const queue = adapter.queues.get(fullChannel);
|
|
136
|
+
queueData.pending = queue?.length || 0;
|
|
137
|
+
} else {
|
|
138
|
+
queueData.pending = null; // Unknown for external adapters
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
channels.push(queueData);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const totalPending = channels.reduce((sum, c) => sum + (c.pending || 0), 0);
|
|
146
|
+
const redisQueues = await getRedisQueues(ctx);
|
|
147
|
+
|
|
148
|
+
ctx.body = {
|
|
149
|
+
adapter: adapterName,
|
|
150
|
+
connected,
|
|
151
|
+
totalChannels: channels.length,
|
|
152
|
+
totalPending,
|
|
153
|
+
redisQueues,
|
|
154
|
+
channels,
|
|
155
|
+
};
|
|
156
|
+
await next();
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* GET /clusterManagerQueue:messages
|
|
161
|
+
* List pending messages in a specific channel (memory adapter only)
|
|
162
|
+
*/
|
|
163
|
+
async messages(ctx: Context, next: () => Promise<void>) {
|
|
164
|
+
const eq = ctx.app.eventQueue;
|
|
165
|
+
if (!eq) {
|
|
166
|
+
ctx.throw(503, 'Event queue is not available');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const { channel, key, source, page = 1, pageSize = 20 } = ctx.action.params;
|
|
170
|
+
if (source === 'redis') {
|
|
171
|
+
const redisKey = String(key || channel || '');
|
|
172
|
+
if (!redisKey || !isKnownRedisQueueKey(redisKey)) {
|
|
173
|
+
ctx.throw(400, 'Invalid Redis queue key');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const redis = await getQueueRedis(ctx);
|
|
177
|
+
if (!redis) {
|
|
178
|
+
ctx.body = {
|
|
179
|
+
data: [],
|
|
180
|
+
meta: { count: 0, page: 1, pageSize: 20, note: 'Redis queue connection is not configured' },
|
|
181
|
+
};
|
|
182
|
+
await next();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const currentPage = Number(page);
|
|
187
|
+
const currentPageSize = Number(pageSize);
|
|
188
|
+
const start = (currentPage - 1) * currentPageSize;
|
|
189
|
+
const end = start + currentPageSize - 1;
|
|
190
|
+
const count = Number(await redis.sendCommand(['LLEN', redisKey])) || 0;
|
|
191
|
+
const rows = (await redis.sendCommand(['LRANGE', redisKey, String(start), String(end)])) as string[];
|
|
192
|
+
|
|
193
|
+
ctx.body = {
|
|
194
|
+
data: rows.map((raw, offset) => parseRedisQueueMessage(raw, redisKey, start + offset)),
|
|
195
|
+
meta: { count, page: currentPage, pageSize: currentPageSize, source: 'redis', key: redisKey },
|
|
196
|
+
};
|
|
197
|
+
await next();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const adapter = (eq as any).adapter;
|
|
202
|
+
|
|
203
|
+
if (!adapter?.queues) {
|
|
204
|
+
ctx.body = {
|
|
205
|
+
data: [],
|
|
206
|
+
meta: { count: 0, page: 1, pageSize: 20, note: 'Message inspection not available for this adapter' },
|
|
207
|
+
};
|
|
208
|
+
await next();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const events = (eq as any).events as Map<string, any>;
|
|
213
|
+
let fullChannel = channel;
|
|
214
|
+
if (events?.has(channel)) {
|
|
215
|
+
fullChannel = eq.getFullChannel(channel, events.get(channel)?.shared);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const queue = adapter.queues.get(fullChannel) || [];
|
|
219
|
+
const start = (Number(page) - 1) * Number(pageSize);
|
|
220
|
+
const slice = queue.slice(start, start + Number(pageSize));
|
|
221
|
+
|
|
222
|
+
ctx.body = {
|
|
223
|
+
data: slice.map((msg: any) => ({
|
|
224
|
+
id: msg.id,
|
|
225
|
+
content: msg.content,
|
|
226
|
+
timestamp: msg.options?.timestamp,
|
|
227
|
+
retried: msg.options?.retried || 0,
|
|
228
|
+
maxRetries: msg.options?.maxRetries || 0,
|
|
229
|
+
})),
|
|
230
|
+
meta: { count: queue.length, page: Number(page), pageSize: Number(pageSize) },
|
|
231
|
+
};
|
|
232
|
+
await next();
|
|
233
|
+
},
|
|
234
|
+
};
|
|
@@ -13,7 +13,7 @@ import type { IOrchestratorAdapter, StackConfig } from '../orchestrator/types';
|
|
|
13
13
|
function getAdapter(ctx: Context): IOrchestratorAdapter {
|
|
14
14
|
const plugin = ctx.app.pm.get('plugin-cluster-manager') as any;
|
|
15
15
|
if (!plugin?.orchestrator) {
|
|
16
|
-
ctx.throw(503, 'Orchestrator adapter not configured. Configure it in
|
|
16
|
+
ctx.throw(503, 'Orchestrator adapter not configured. Configure it in Cluster Manager settings.');
|
|
17
17
|
}
|
|
18
18
|
return plugin.orchestrator;
|
|
19
19
|
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { Context } from '@nocobase/actions';
|
|
2
|
+
|
|
3
|
+
const PROTECTED_PLUGIN_NAMES = new Set(['nocobase', 'plugin-cluster-manager']);
|
|
4
|
+
const PROTECTED_PACKAGE_NAMES = new Set(['nocobase', '@nocobase/preset-nocobase', 'plugin-cluster-manager']);
|
|
5
|
+
|
|
6
|
+
function getPayload(ctx: Context) {
|
|
7
|
+
return (ctx.action.params.values || (ctx as any).request?.body?.values || (ctx as any).request?.body || {}) as any;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function pluginDisplayName(plugin: any) {
|
|
11
|
+
return plugin.displayName || plugin.name || plugin.packageName;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isProtectedPlugin(plugin: any) {
|
|
15
|
+
return PROTECTED_PLUGIN_NAMES.has(plugin.name) || PROTECTED_PACKAGE_NAMES.has(plugin.packageName);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function getApplicationPlugins(ctx: Context) {
|
|
19
|
+
const repo = ctx.db.getRepository('applicationPlugins');
|
|
20
|
+
const rows = await repo.find({ sort: ['name'] });
|
|
21
|
+
return rows.map((row: any) => row.toJSON());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function findPlugin(ctx: Context, requestedName?: string) {
|
|
25
|
+
const name = String(requestedName || '').trim();
|
|
26
|
+
if (!name) {
|
|
27
|
+
ctx.throw(400, 'Plugin name is required.');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const plugins = await getApplicationPlugins(ctx);
|
|
31
|
+
const plugin = plugins.find((item: any) => item.name === name || item.packageName === name);
|
|
32
|
+
if (!plugin) {
|
|
33
|
+
ctx.throw(404, `Plugin "${name}" was not found in applicationPlugins.`);
|
|
34
|
+
}
|
|
35
|
+
return plugin;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function getLoadedPluginInfo(ctx: Context, plugin: any, locale: string): Promise<any> {
|
|
39
|
+
const pm = (ctx.app as any).pm;
|
|
40
|
+
const instance = pm?.get?.(plugin.name) || pm?.get?.(plugin.packageName);
|
|
41
|
+
if (!instance) {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
return await instance.toJSON({ locale, withOutOpenFile: true });
|
|
47
|
+
} catch {
|
|
48
|
+
return {
|
|
49
|
+
name: instance.name,
|
|
50
|
+
packageName: instance.options?.packageName,
|
|
51
|
+
displayName: instance.options?.packageJson?.displayName,
|
|
52
|
+
description: instance.options?.packageJson?.description,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function getPackageInfo(ctx: Context, plugin: any, locale: string): Promise<any> {
|
|
58
|
+
const loadedInfo = await getLoadedPluginInfo(ctx, plugin, locale);
|
|
59
|
+
if (loadedInfo.displayName || loadedInfo.description) {
|
|
60
|
+
return loadedInfo;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const pmCtor = (ctx.app as any).pm?.constructor as any;
|
|
65
|
+
const pkgJson = await pmCtor.getPackageJson(plugin.packageName);
|
|
66
|
+
return {
|
|
67
|
+
displayName: pkgJson?.[`displayName.${locale}`] || pkgJson?.displayName || plugin.name,
|
|
68
|
+
description: pkgJson?.[`description.${locale}`] || pkgJson?.description,
|
|
69
|
+
keywords: pkgJson?.keywords,
|
|
70
|
+
};
|
|
71
|
+
} catch {
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const pluginOperationsActions = {
|
|
77
|
+
async list(ctx: Context, next: () => Promise<void>) {
|
|
78
|
+
const locale = (ctx as any).getCurrentLocale?.() || 'en-US';
|
|
79
|
+
const plugins = await getApplicationPlugins(ctx);
|
|
80
|
+
const data = await Promise.all(
|
|
81
|
+
plugins.map(async (plugin: any) => {
|
|
82
|
+
const info = await getPackageInfo(ctx, plugin, locale);
|
|
83
|
+
const loaded = Boolean((ctx.app as any).pm?.get?.(plugin.name) || (ctx.app as any).pm?.get?.(plugin.packageName));
|
|
84
|
+
return {
|
|
85
|
+
...plugin,
|
|
86
|
+
displayName: info.displayName || plugin.name,
|
|
87
|
+
description: info.description || '',
|
|
88
|
+
keywords: info.keywords || [],
|
|
89
|
+
loaded,
|
|
90
|
+
protected: isProtectedPlugin(plugin),
|
|
91
|
+
};
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
ctx.body = {
|
|
96
|
+
data,
|
|
97
|
+
meta: { count: data.length },
|
|
98
|
+
};
|
|
99
|
+
await next();
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
async forceDisable(ctx: Context, next: () => Promise<void>) {
|
|
103
|
+
const payload = getPayload(ctx);
|
|
104
|
+
const plugin = await findPlugin(ctx, payload.name || payload.packageName || ctx.action.params.filterByTk);
|
|
105
|
+
if (isProtectedPlugin(plugin)) {
|
|
106
|
+
ctx.throw(400, `Plugin "${pluginDisplayName(plugin)}" is protected and cannot be disabled from Cluster Manager.`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const repo = ctx.db.getRepository('applicationPlugins');
|
|
110
|
+
await repo.update({
|
|
111
|
+
filter: { name: plugin.name },
|
|
112
|
+
values: { enabled: false },
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const instance = (ctx.app as any).pm?.get?.(plugin.name) || (ctx.app as any).pm?.get?.(plugin.packageName);
|
|
116
|
+
if (instance) {
|
|
117
|
+
instance.enabled = false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
ctx.body = {
|
|
121
|
+
success: true,
|
|
122
|
+
name: plugin.name,
|
|
123
|
+
packageName: plugin.packageName,
|
|
124
|
+
restartRequired: true,
|
|
125
|
+
message: `Plugin "${pluginDisplayName(plugin)}" was force disabled. Restart or reload the app to fully unload it.`,
|
|
126
|
+
};
|
|
127
|
+
await next();
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
async forceRemove(ctx: Context, next: () => Promise<void>) {
|
|
131
|
+
const payload = getPayload(ctx);
|
|
132
|
+
const plugin = await findPlugin(ctx, payload.name || payload.packageName || ctx.action.params.filterByTk);
|
|
133
|
+
if (isProtectedPlugin(plugin)) {
|
|
134
|
+
ctx.throw(400, `Plugin "${pluginDisplayName(plugin)}" is protected and cannot be removed from Cluster Manager.`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const repo = ctx.db.getRepository('applicationPlugins');
|
|
138
|
+
await repo.destroy({
|
|
139
|
+
filter: { name: plugin.name },
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
ctx.body = {
|
|
143
|
+
success: true,
|
|
144
|
+
name: plugin.name,
|
|
145
|
+
packageName: plugin.packageName,
|
|
146
|
+
restartRequired: true,
|
|
147
|
+
message: `Plugin "${pluginDisplayName(plugin)}" was force removed from the application registry. Package files were not deleted.`,
|
|
148
|
+
};
|
|
149
|
+
await next();
|
|
150
|
+
},
|
|
151
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DUMMY COLLECTION
|
|
3
|
+
* Keeps the `clusterManagerPlugins` resourcer compatible with NocoBase
|
|
4
|
+
* workflow/ACL collection lookups. The actual plugin records live in
|
|
5
|
+
* the core `applicationPlugins` collection.
|
|
6
|
+
*/
|
|
7
|
+
export default {
|
|
8
|
+
name: 'clusterManagerPlugins',
|
|
9
|
+
dumpRules: 'skip',
|
|
10
|
+
autoGenId: true,
|
|
11
|
+
createdAt: false,
|
|
12
|
+
updatedAt: false,
|
|
13
|
+
fields: [
|
|
14
|
+
{
|
|
15
|
+
name: 'name',
|
|
16
|
+
type: 'string',
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
};
|
package/src/server/plugin.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { RedisPubSubAdapter } from './adapters/redis-pubsub-adapter';
|
|
|
15
15
|
import { RedisNodeRegistry } from './adapters/redis-node-registry';
|
|
16
16
|
import { RedisLockAdapter } from './adapters/redis-lock-adapter';
|
|
17
17
|
import { orchestratorActions } from './actions/orchestrator';
|
|
18
|
+
import { pluginOperationsActions } from './actions/plugin-operations';
|
|
18
19
|
import type { IOrchestratorAdapter } from './orchestrator/types';
|
|
19
20
|
import { DockerAdapter } from './orchestrator/docker-adapter';
|
|
20
21
|
import { K8sAdapter } from './orchestrator/k8s-adapter';
|
|
@@ -240,11 +241,17 @@ export class PluginClusterManagerServer extends Plugin {
|
|
|
240
241
|
actions: cacheMonitorActions,
|
|
241
242
|
});
|
|
242
243
|
|
|
243
|
-
// Package manager (installs apt/npm/python packages across nodes)
|
|
244
|
-
this.app.resourcer.define({
|
|
245
|
-
name: 'workerPackages',
|
|
246
|
-
actions: packageManagerActions,
|
|
247
|
-
});
|
|
244
|
+
// Package manager (installs apt/npm/python packages across nodes)
|
|
245
|
+
this.app.resourcer.define({
|
|
246
|
+
name: 'workerPackages',
|
|
247
|
+
actions: packageManagerActions,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Plugin operations (force disable/remove application plugin records)
|
|
251
|
+
this.app.resourcer.define({
|
|
252
|
+
name: 'clusterManagerPlugins',
|
|
253
|
+
actions: pluginOperationsActions,
|
|
254
|
+
});
|
|
248
255
|
|
|
249
256
|
// Install ACL cache middleware inside the ACL chain so cached permissions are not overwritten.
|
|
250
257
|
const aclCacheMiddleware = createAclCacheMiddleware(this.app);
|
|
@@ -279,11 +286,12 @@ export class PluginClusterManagerServer extends Plugin {
|
|
|
279
286
|
'clusterManagerQueue:*',
|
|
280
287
|
'clusterManagerLock:*',
|
|
281
288
|
'clusterManagerCacheMgr:*',
|
|
282
|
-
'workerOrchestrator:*',
|
|
283
|
-
'orchestratorStacks:*',
|
|
284
|
-
'workerPackages:*',
|
|
285
|
-
|
|
286
|
-
|
|
289
|
+
'workerOrchestrator:*',
|
|
290
|
+
'orchestratorStacks:*',
|
|
291
|
+
'workerPackages:*',
|
|
292
|
+
'clusterManagerPlugins:*',
|
|
293
|
+
],
|
|
294
|
+
});
|
|
287
295
|
|
|
288
296
|
// ── Container Orchestrator ──
|
|
289
297
|
await this.initOrchestrator();
|
|
@@ -365,16 +373,16 @@ export class PluginClusterManagerServer extends Plugin {
|
|
|
365
373
|
} else {
|
|
366
374
|
// Fall back to env var for initial setup
|
|
367
375
|
const envAdapter = process.env.ORCHESTRATOR_ADAPTER;
|
|
368
|
-
if (envAdapter && envAdapter !== 'none') {
|
|
369
|
-
await this.connectAdapter({
|
|
370
|
-
adapterType: envAdapter,
|
|
371
|
-
dockerSocketPath: process.env.DOCKER_SOCKET || '/var/run/docker.sock',
|
|
372
|
-
k8sNamespace: 'nocobase',
|
|
373
|
-
});
|
|
374
|
-
} else {
|
|
375
|
-
this.app.logger.info('[Orchestrator] No adapter configured — configurable via
|
|
376
|
-
}
|
|
377
|
-
}
|
|
376
|
+
if (envAdapter && envAdapter !== 'none') {
|
|
377
|
+
await this.connectAdapter({
|
|
378
|
+
adapterType: envAdapter,
|
|
379
|
+
dockerSocketPath: process.env.DOCKER_SOCKET || '/var/run/docker.sock',
|
|
380
|
+
k8sNamespace: 'nocobase',
|
|
381
|
+
});
|
|
382
|
+
} else {
|
|
383
|
+
this.app.logger.info('[Orchestrator] No adapter configured — configurable via Cluster Manager UI');
|
|
384
|
+
}
|
|
385
|
+
}
|
|
378
386
|
} catch (err: any) {
|
|
379
387
|
this.app.logger.warn(`[Orchestrator] Could not load settings: ${err.message}`);
|
|
380
388
|
}
|
package/dist/client/index.d.ts
DELETED
package/dist/client/utils.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared i18n hook for the cluster-manager plugin.
|
|
3
|
-
*/
|
|
4
|
-
export declare function useT(): (key: string) => string;
|
|
5
|
-
/**
|
|
6
|
-
* Format bytes into human-readable string (B, KB, MB, GB).
|
|
7
|
-
*/
|
|
8
|
-
export declare function formatBytes(bytes: number): string;
|
|
9
|
-
/**
|
|
10
|
-
* Format seconds into human-readable uptime string (e.g., "2d 5h 30m").
|
|
11
|
-
*/
|
|
12
|
-
export declare function formatUptime(seconds: number): string;
|
package/dist/index.d.ts
DELETED