plugin-cluster-manager 1.1.7 → 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/utils.d.ts +12 -0
- 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/server/actions/acl-cache.ts +2 -2
- package/src/server/plugin.ts +15 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin settings stored in DB — configurable via NocoBase admin UI.
|
|
3
|
+
* Replaces env-var-only configuration for orchestrator adapter selection.
|
|
4
|
+
*/
|
|
5
|
+
declare const _default: {
|
|
6
|
+
name: string;
|
|
7
|
+
autoGenId: boolean;
|
|
8
|
+
createdAt: boolean;
|
|
9
|
+
updatedAt: boolean;
|
|
10
|
+
fields: ({
|
|
11
|
+
name: string;
|
|
12
|
+
type: string;
|
|
13
|
+
defaultValue: string;
|
|
14
|
+
interface: string;
|
|
15
|
+
uiSchema: {
|
|
16
|
+
title: string;
|
|
17
|
+
'x-component': string;
|
|
18
|
+
enum: {
|
|
19
|
+
value: string;
|
|
20
|
+
label: string;
|
|
21
|
+
}[];
|
|
22
|
+
description?: undefined;
|
|
23
|
+
};
|
|
24
|
+
} | {
|
|
25
|
+
name: string;
|
|
26
|
+
type: string;
|
|
27
|
+
defaultValue: string;
|
|
28
|
+
interface: string;
|
|
29
|
+
uiSchema: {
|
|
30
|
+
title: string;
|
|
31
|
+
'x-component': string;
|
|
32
|
+
enum?: undefined;
|
|
33
|
+
description?: undefined;
|
|
34
|
+
};
|
|
35
|
+
} | {
|
|
36
|
+
name: string;
|
|
37
|
+
type: string;
|
|
38
|
+
interface: string;
|
|
39
|
+
uiSchema: {
|
|
40
|
+
title: string;
|
|
41
|
+
description: string;
|
|
42
|
+
'x-component': string;
|
|
43
|
+
enum?: undefined;
|
|
44
|
+
};
|
|
45
|
+
defaultValue?: undefined;
|
|
46
|
+
} | {
|
|
47
|
+
name: string;
|
|
48
|
+
type: string;
|
|
49
|
+
defaultValue: string;
|
|
50
|
+
interface: string;
|
|
51
|
+
uiSchema: {
|
|
52
|
+
title: string;
|
|
53
|
+
description: string;
|
|
54
|
+
'x-component': string;
|
|
55
|
+
enum?: undefined;
|
|
56
|
+
};
|
|
57
|
+
})[];
|
|
58
|
+
};
|
|
59
|
+
export default _default;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
declare const _default: {
|
|
2
|
+
name: string;
|
|
3
|
+
autoGenId: boolean;
|
|
4
|
+
createdAt: boolean;
|
|
5
|
+
updatedAt: boolean;
|
|
6
|
+
fields: ({
|
|
7
|
+
name: string;
|
|
8
|
+
type: string;
|
|
9
|
+
unique: boolean;
|
|
10
|
+
interface: string;
|
|
11
|
+
uiSchema: {
|
|
12
|
+
title: string;
|
|
13
|
+
'x-component': string;
|
|
14
|
+
enum?: undefined;
|
|
15
|
+
};
|
|
16
|
+
defaultValue?: undefined;
|
|
17
|
+
} | {
|
|
18
|
+
name: string;
|
|
19
|
+
type: string;
|
|
20
|
+
defaultValue: string;
|
|
21
|
+
interface: string;
|
|
22
|
+
uiSchema: {
|
|
23
|
+
title: string;
|
|
24
|
+
'x-component': string;
|
|
25
|
+
enum: {
|
|
26
|
+
value: string;
|
|
27
|
+
label: string;
|
|
28
|
+
}[];
|
|
29
|
+
};
|
|
30
|
+
unique?: undefined;
|
|
31
|
+
} | {
|
|
32
|
+
name: string;
|
|
33
|
+
type: string;
|
|
34
|
+
interface: string;
|
|
35
|
+
uiSchema: {
|
|
36
|
+
title: string;
|
|
37
|
+
'x-component': string;
|
|
38
|
+
enum?: undefined;
|
|
39
|
+
};
|
|
40
|
+
unique?: undefined;
|
|
41
|
+
defaultValue?: undefined;
|
|
42
|
+
} | {
|
|
43
|
+
name: string;
|
|
44
|
+
type: string;
|
|
45
|
+
defaultValue: {};
|
|
46
|
+
interface: string;
|
|
47
|
+
uiSchema: {
|
|
48
|
+
title: string;
|
|
49
|
+
'x-component'?: undefined;
|
|
50
|
+
enum?: undefined;
|
|
51
|
+
};
|
|
52
|
+
unique?: undefined;
|
|
53
|
+
} | {
|
|
54
|
+
name: string;
|
|
55
|
+
type: string;
|
|
56
|
+
defaultValue: any[];
|
|
57
|
+
interface: string;
|
|
58
|
+
uiSchema: {
|
|
59
|
+
title: string;
|
|
60
|
+
'x-component'?: undefined;
|
|
61
|
+
enum?: undefined;
|
|
62
|
+
};
|
|
63
|
+
unique?: undefined;
|
|
64
|
+
} | {
|
|
65
|
+
name: string;
|
|
66
|
+
type: string;
|
|
67
|
+
defaultValue: number;
|
|
68
|
+
interface: string;
|
|
69
|
+
uiSchema: {
|
|
70
|
+
title: string;
|
|
71
|
+
'x-component'?: undefined;
|
|
72
|
+
enum?: undefined;
|
|
73
|
+
};
|
|
74
|
+
unique?: undefined;
|
|
75
|
+
} | {
|
|
76
|
+
name: string;
|
|
77
|
+
type: string;
|
|
78
|
+
defaultValue: boolean;
|
|
79
|
+
interface: string;
|
|
80
|
+
uiSchema: {
|
|
81
|
+
title: string;
|
|
82
|
+
'x-component'?: undefined;
|
|
83
|
+
enum?: undefined;
|
|
84
|
+
};
|
|
85
|
+
unique?: undefined;
|
|
86
|
+
} | {
|
|
87
|
+
name: string;
|
|
88
|
+
type: string;
|
|
89
|
+
defaultValue: string;
|
|
90
|
+
unique?: undefined;
|
|
91
|
+
interface?: undefined;
|
|
92
|
+
uiSchema?: undefined;
|
|
93
|
+
} | {
|
|
94
|
+
name: string;
|
|
95
|
+
type: string;
|
|
96
|
+
unique?: undefined;
|
|
97
|
+
interface?: undefined;
|
|
98
|
+
uiSchema?: undefined;
|
|
99
|
+
defaultValue?: undefined;
|
|
100
|
+
})[];
|
|
101
|
+
};
|
|
102
|
+
export default _default;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DUMMY COLLECTION
|
|
3
|
+
* This collection is created to prevent NocoBase workflow/ACL from throwing the error:
|
|
4
|
+
* '[Workflow pre-action]: collection "workerOrchestrator" not found'
|
|
5
|
+
*
|
|
6
|
+
* Since 'workerOrchestrator' is registered as a resourcer in plugin.ts, NocoBase
|
|
7
|
+
* implicitly looks for a collection with the same name.
|
|
8
|
+
* We set dumpRules: 'skip' and avoid timestamps to ensure this collection
|
|
9
|
+
* is completely ignored during backups/migrations and doesn't pollute the actual DB.
|
|
10
|
+
*/
|
|
11
|
+
declare const _default: {
|
|
12
|
+
name: string;
|
|
13
|
+
dumpRules: string;
|
|
14
|
+
autoGenId: boolean;
|
|
15
|
+
createdAt: boolean;
|
|
16
|
+
updatedAt: boolean;
|
|
17
|
+
fields: {
|
|
18
|
+
name: string;
|
|
19
|
+
type: string;
|
|
20
|
+
}[];
|
|
21
|
+
};
|
|
22
|
+
export default _default;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DUMMY COLLECTION
|
|
3
|
+
* This collection is created to prevent NocoBase workflow/ACL from throwing the error:
|
|
4
|
+
* '[Workflow pre-action]: collection "workerPackages" not found'
|
|
5
|
+
*
|
|
6
|
+
* Since 'workerPackages' is registered as a resourcer in plugin.ts, NocoBase
|
|
7
|
+
* implicitly looks for a collection with the same name.
|
|
8
|
+
* We set dumpRules: 'skip' and avoid timestamps to ensure this collection
|
|
9
|
+
* is completely ignored during backups/migrations and doesn't pollute the actual DB.
|
|
10
|
+
*/
|
|
11
|
+
declare const _default: {
|
|
12
|
+
name: string;
|
|
13
|
+
dumpRules: string;
|
|
14
|
+
autoGenId: boolean;
|
|
15
|
+
createdAt: boolean;
|
|
16
|
+
updatedAt: boolean;
|
|
17
|
+
fields: {
|
|
18
|
+
name: string;
|
|
19
|
+
type: string;
|
|
20
|
+
}[];
|
|
21
|
+
};
|
|
22
|
+
export default _default;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './plugin';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import Application from '@nocobase/server';
|
|
2
|
+
type TargetRole = 'app' | 'worker' | 'sandbox' | 'all';
|
|
3
|
+
interface InstallPayload {
|
|
4
|
+
targetRole: TargetRole;
|
|
5
|
+
packages: {
|
|
6
|
+
apt?: string[];
|
|
7
|
+
npm?: string[];
|
|
8
|
+
python?: string[];
|
|
9
|
+
};
|
|
10
|
+
registryConfig?: {
|
|
11
|
+
aptMirrorUrl?: string;
|
|
12
|
+
npmRegistryUrl?: string;
|
|
13
|
+
pypiIndexUrl?: string;
|
|
14
|
+
pypiTrustedHost?: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export declare class PackageManager {
|
|
18
|
+
private app;
|
|
19
|
+
constructor(app: Application);
|
|
20
|
+
/**
|
|
21
|
+
* Called from REST action when admin clicks "Install Packages".
|
|
22
|
+
* Publishes task to all nodes via PubSub. Nodes will filter by targetRole.
|
|
23
|
+
*/
|
|
24
|
+
dispatchInstall(payload: InstallPayload): Promise<string>;
|
|
25
|
+
executeInstall(payload: InstallPayload): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Run a command without a shell so registry URLs and package names are not re-parsed as shell syntax.
|
|
28
|
+
*/
|
|
29
|
+
private runCommand;
|
|
30
|
+
private getMissingPackages;
|
|
31
|
+
private isPackageInstalled;
|
|
32
|
+
private runStatus;
|
|
33
|
+
private getAptOsInfo;
|
|
34
|
+
private buildAptSources;
|
|
35
|
+
private configureAptMirror;
|
|
36
|
+
private moveIfExists;
|
|
37
|
+
private updateInstallStatus;
|
|
38
|
+
}
|
|
39
|
+
export {};
|
|
@@ -43,7 +43,7 @@ var import_child_process = require("child_process");
|
|
|
43
43
|
var import_redis = require("../utils/redis");
|
|
44
44
|
var import_fs = require("fs");
|
|
45
45
|
var import_path = __toESM(require("path"));
|
|
46
|
-
const SAFE_PKG_RE = /^[a-zA-Z0-9_
|
|
46
|
+
const SAFE_PKG_RE = /^(?:[a-zA-Z0-9_.@/-]|\[|\])+$/;
|
|
47
47
|
const INSTALL_CHANNEL = "cluster-manager.install-packages";
|
|
48
48
|
function sanitizePkg(name) {
|
|
49
49
|
if (typeof name !== "string") {
|
|
@@ -99,6 +99,35 @@ function formatCommand(command, args) {
|
|
|
99
99
|
return /^[a-zA-Z0-9_./:@=-]+$/.test(value) ? value : JSON.stringify(value);
|
|
100
100
|
}).join(" ");
|
|
101
101
|
}
|
|
102
|
+
function parseOsRelease(content) {
|
|
103
|
+
const values = {};
|
|
104
|
+
for (const line of content.split("\n")) {
|
|
105
|
+
const match = line.match(/^([A-Z0-9_]+)=(.*)$/);
|
|
106
|
+
if (!match) continue;
|
|
107
|
+
values[match[1]] = match[2].replace(/^"|"$/g, "");
|
|
108
|
+
}
|
|
109
|
+
return values;
|
|
110
|
+
}
|
|
111
|
+
function normalizeMirrorUrl(value) {
|
|
112
|
+
return value.endsWith("/") ? value : `${value}/`;
|
|
113
|
+
}
|
|
114
|
+
function isInternalCacheMirror(url) {
|
|
115
|
+
return url.hostname === "nginx-cache-registry";
|
|
116
|
+
}
|
|
117
|
+
function resolveAptMirrorForOs(aptMirrorUrl, osInfo, logs) {
|
|
118
|
+
const url = new URL(normalizeMirrorUrl(aptMirrorUrl));
|
|
119
|
+
const original = url.toString();
|
|
120
|
+
if (isInternalCacheMirror(url) && osInfo.id === "debian" && url.pathname === "/ubuntu/") {
|
|
121
|
+
url.pathname = "/debian/";
|
|
122
|
+
} else if (isInternalCacheMirror(url) && osInfo.id === "ubuntu" && url.pathname === "/debian/") {
|
|
123
|
+
url.pathname = "/ubuntu/";
|
|
124
|
+
}
|
|
125
|
+
const resolved = url.toString();
|
|
126
|
+
if (resolved !== original) {
|
|
127
|
+
logs.push(`Adjusted APT mirror for ${osInfo.id}: ${redactUrl(resolved)}`);
|
|
128
|
+
}
|
|
129
|
+
return resolved;
|
|
130
|
+
}
|
|
102
131
|
class PackageManager {
|
|
103
132
|
constructor(app) {
|
|
104
133
|
this.app = app;
|
|
@@ -143,7 +172,7 @@ class PackageManager {
|
|
|
143
172
|
if (registryConfig.aptMirrorUrl) {
|
|
144
173
|
const aptMirrorUrl = sanitizeHttpUrl(registryConfig.aptMirrorUrl, "APT mirror URL");
|
|
145
174
|
logs.push(`Applying APT mirror: ${redactUrl(aptMirrorUrl)}`);
|
|
146
|
-
await this.configureAptMirror(aptMirrorUrl);
|
|
175
|
+
await this.configureAptMirror(aptMirrorUrl, logs);
|
|
147
176
|
}
|
|
148
177
|
await this.updateInstallStatus("running", 20, "Installing APT packages...", logs);
|
|
149
178
|
await this.runCommand("apt-get", ["update", "-qq"], "Updating APT package index...", logs, 12e5);
|
|
@@ -245,11 +274,11 @@ ${logs.join("\n")}`);
|
|
|
245
274
|
let stdout = "";
|
|
246
275
|
let stderr = "";
|
|
247
276
|
let settled = false;
|
|
248
|
-
|
|
277
|
+
const state = {};
|
|
249
278
|
const finish = (error) => {
|
|
250
279
|
if (settled) return;
|
|
251
280
|
settled = true;
|
|
252
|
-
clearTimeout(timer);
|
|
281
|
+
if (state.timer) clearTimeout(state.timer);
|
|
253
282
|
if (stdout) logs.push(stdout.slice(0, 500));
|
|
254
283
|
if (stderr) logs.push(`WARN: ${stderr.slice(0, 300)}`);
|
|
255
284
|
if (error) {
|
|
@@ -259,7 +288,7 @@ ${logs.join("\n")}`);
|
|
|
259
288
|
resolve();
|
|
260
289
|
}
|
|
261
290
|
};
|
|
262
|
-
timer = setTimeout(() => {
|
|
291
|
+
state.timer = setTimeout(() => {
|
|
263
292
|
child.kill("SIGTERM");
|
|
264
293
|
finish(new Error(`${command} timed out after ${timeoutMs}ms`));
|
|
265
294
|
}, timeoutMs);
|
|
@@ -312,14 +341,14 @@ ${logs.join("\n")}`);
|
|
|
312
341
|
const child = (0, import_child_process.spawn)(command, args, { stdio: ["ignore", "pipe", "ignore"] });
|
|
313
342
|
let stdout = "";
|
|
314
343
|
let settled = false;
|
|
315
|
-
|
|
344
|
+
const state = {};
|
|
316
345
|
const finish = (ok) => {
|
|
317
346
|
if (settled) return;
|
|
318
347
|
settled = true;
|
|
319
|
-
clearTimeout(timer);
|
|
348
|
+
if (state.timer) clearTimeout(state.timer);
|
|
320
349
|
resolve(ok);
|
|
321
350
|
};
|
|
322
|
-
timer = setTimeout(() => {
|
|
351
|
+
state.timer = setTimeout(() => {
|
|
323
352
|
child.kill("SIGTERM");
|
|
324
353
|
finish(false);
|
|
325
354
|
}, timeoutMs);
|
|
@@ -330,7 +359,31 @@ ${logs.join("\n")}`);
|
|
|
330
359
|
child.on("close", (code) => finish(code === 0 && (isSuccess ? isSuccess(stdout) : true)));
|
|
331
360
|
});
|
|
332
361
|
}
|
|
333
|
-
async
|
|
362
|
+
async getAptOsInfo() {
|
|
363
|
+
const values = parseOsRelease(await import_fs.promises.readFile("/etc/os-release", "utf8"));
|
|
364
|
+
const id = (values.ID || "").toLowerCase();
|
|
365
|
+
const codename = values.VERSION_CODENAME || values.UBUNTU_CODENAME;
|
|
366
|
+
if (!id || !codename) {
|
|
367
|
+
throw new Error("Cannot detect OS ID/version codename from /etc/os-release for APT mirror configuration.");
|
|
368
|
+
}
|
|
369
|
+
return { id, codename };
|
|
370
|
+
}
|
|
371
|
+
buildAptSources(aptMirrorUrl, osInfo) {
|
|
372
|
+
const mirror = normalizeMirrorUrl(aptMirrorUrl);
|
|
373
|
+
if (osInfo.id === "ubuntu") {
|
|
374
|
+
return `deb ${mirror} ${osInfo.codename} main universe restricted multiverse
|
|
375
|
+
`;
|
|
376
|
+
}
|
|
377
|
+
if (osInfo.id === "debian") {
|
|
378
|
+
return `deb ${mirror} ${osInfo.codename} main contrib non-free non-free-firmware
|
|
379
|
+
`;
|
|
380
|
+
}
|
|
381
|
+
return `deb ${mirror} ${osInfo.codename} main
|
|
382
|
+
`;
|
|
383
|
+
}
|
|
384
|
+
async configureAptMirror(aptMirrorUrl, logs) {
|
|
385
|
+
const osInfo = await this.getAptOsInfo();
|
|
386
|
+
const resolvedMirrorUrl = resolveAptMirrorForOs(aptMirrorUrl, osInfo, logs);
|
|
334
387
|
const backupDir = "/etc/apt/sources.list.d.bak";
|
|
335
388
|
await import_fs.promises.mkdir(backupDir, { recursive: true });
|
|
336
389
|
await this.moveIfExists("/etc/apt/sources.list", import_path.default.join(backupDir, `sources.list.${Date.now()}.bak`));
|
|
@@ -342,8 +395,7 @@ ${logs.join("\n")}`);
|
|
|
342
395
|
}
|
|
343
396
|
} catch {
|
|
344
397
|
}
|
|
345
|
-
await import_fs.promises.writeFile("/etc/apt/sources.list",
|
|
346
|
-
`, "utf8");
|
|
398
|
+
await import_fs.promises.writeFile("/etc/apt/sources.list", this.buildAptSources(resolvedMirrorUrl, osInfo), "utf8");
|
|
347
399
|
}
|
|
348
400
|
async moveIfExists(from, to) {
|
|
349
401
|
try {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker Adapter — MVP implementation
|
|
3
|
+
*
|
|
4
|
+
* Uses dockerode to communicate with the Docker daemon via unix socket.
|
|
5
|
+
* Containers are tagged with label `orchestrator.stack=<stackName>` for identification.
|
|
6
|
+
*
|
|
7
|
+
* Security: Docker socket = root on host. Only admin users should access
|
|
8
|
+
* orchestrator actions (enforced by ACL in plugin.ts).
|
|
9
|
+
*/
|
|
10
|
+
import type { IOrchestratorAdapter, ContainerInfo, ScaleResult, ContainerStats, StackConfig } from './types';
|
|
11
|
+
export declare class DockerAdapter implements IOrchestratorAdapter {
|
|
12
|
+
readonly name = "docker";
|
|
13
|
+
private docker;
|
|
14
|
+
private workerLabels;
|
|
15
|
+
constructor(options?: {
|
|
16
|
+
socketPath?: string;
|
|
17
|
+
host?: string;
|
|
18
|
+
port?: number;
|
|
19
|
+
workerLabelSelector?: string;
|
|
20
|
+
});
|
|
21
|
+
ping(): Promise<boolean>;
|
|
22
|
+
listContainers(stack: StackConfig): Promise<ContainerInfo[]>;
|
|
23
|
+
assertManagedByStack(stack: StackConfig, containerId: string): Promise<void>;
|
|
24
|
+
scale(stack: StackConfig, replicas: number): Promise<ScaleResult>;
|
|
25
|
+
startContainer(containerId: string): Promise<void>;
|
|
26
|
+
stopContainer(containerId: string, timeoutSecs?: number): Promise<void>;
|
|
27
|
+
removeContainer(containerId: string): Promise<void>;
|
|
28
|
+
getStats(containerId: string): Promise<ContainerStats>;
|
|
29
|
+
getLogs(containerId: string, tail?: number): Promise<string>;
|
|
30
|
+
listNetworks(): Promise<{
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
}[]>;
|
|
34
|
+
private mapState;
|
|
35
|
+
private buildEnvArray;
|
|
36
|
+
private buildLabelFilters;
|
|
37
|
+
private parseLabelSelector;
|
|
38
|
+
private labelsMatch;
|
|
39
|
+
private parseMemory;
|
|
40
|
+
private demuxDockerLogs;
|
|
41
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kubernetes Adapter
|
|
3
|
+
*
|
|
4
|
+
* Manages worker Deployments/Pods via the K8s API using in-cluster ServiceAccount
|
|
5
|
+
* or kubeconfig. Only manages resources labeled with `orchestrator.stack=<stackName>`.
|
|
6
|
+
*
|
|
7
|
+
* Prerequisites:
|
|
8
|
+
* - ServiceAccount with RBAC: get/list/create/patch/update on Deployments and Deployments/scale
|
|
9
|
+
* - get/list/delete on Pods and get on Pods/log
|
|
10
|
+
* - Or KUBECONFIG env var pointing to a valid kubeconfig file
|
|
11
|
+
*/
|
|
12
|
+
import type { IOrchestratorAdapter, ContainerInfo, ScaleResult, ContainerStats, StackConfig } from './types';
|
|
13
|
+
export declare class K8sAdapter implements IOrchestratorAdapter {
|
|
14
|
+
readonly name = "kubernetes";
|
|
15
|
+
private appsApi;
|
|
16
|
+
private coreApi;
|
|
17
|
+
private kc;
|
|
18
|
+
private namespace;
|
|
19
|
+
private workerLabelSelector;
|
|
20
|
+
private workerLabels;
|
|
21
|
+
constructor(options?: {
|
|
22
|
+
kubeconfig?: string;
|
|
23
|
+
context?: string;
|
|
24
|
+
namespace?: string;
|
|
25
|
+
workerLabelSelector?: string;
|
|
26
|
+
});
|
|
27
|
+
ping(): Promise<boolean>;
|
|
28
|
+
listContainers(stack: StackConfig): Promise<ContainerInfo[]>;
|
|
29
|
+
scale(stack: StackConfig, replicas: number): Promise<ScaleResult>;
|
|
30
|
+
startContainer(containerId: string): Promise<void>;
|
|
31
|
+
stopContainer(containerId: string): Promise<void>;
|
|
32
|
+
removeContainer(containerId: string): Promise<void>;
|
|
33
|
+
getStats(containerId: string): Promise<ContainerStats>;
|
|
34
|
+
getLogs(containerId: string, tail?: number): Promise<string>;
|
|
35
|
+
assertManagedByStack(stack: StackConfig, containerId: string): Promise<void>;
|
|
36
|
+
private parsePodRef;
|
|
37
|
+
private buildDeployment;
|
|
38
|
+
private buildWorkerLabels;
|
|
39
|
+
private buildContainerEnv;
|
|
40
|
+
private buildResources;
|
|
41
|
+
private cleanArray;
|
|
42
|
+
private isEmptyValue;
|
|
43
|
+
private isNotFound;
|
|
44
|
+
private assertDeploymentManagedByStack;
|
|
45
|
+
private joinSelectors;
|
|
46
|
+
private parseLabelSelector;
|
|
47
|
+
private labelsMatch;
|
|
48
|
+
private mapPodPhase;
|
|
49
|
+
private parseK8sMemory;
|
|
50
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Leader Election using native Redis commands
|
|
3
|
+
*
|
|
4
|
+
* Ensures only ONE app instance runs orchestrator write operations (scale, start, stop).
|
|
5
|
+
* Other instances remain read-only followers.
|
|
6
|
+
*
|
|
7
|
+
* Flow:
|
|
8
|
+
* 1. After app starts, tryBecomeLeader() attempts to acquire the Redis lock using SET NX PX.
|
|
9
|
+
* 2. If acquired → this node is leader; a renewal timer extends the lock periodically using a Lua script.
|
|
10
|
+
* 3. If not acquired → this node is follower; it retries periodically.
|
|
11
|
+
* 4. On app stop → release the lock gracefully using a Lua script.
|
|
12
|
+
* 5. On crash → lock auto-expires after TTL.
|
|
13
|
+
*/
|
|
14
|
+
export declare class LeaderElection {
|
|
15
|
+
private app;
|
|
16
|
+
private renewTimer;
|
|
17
|
+
private retryTimer;
|
|
18
|
+
private _isLeader;
|
|
19
|
+
private _leaderId;
|
|
20
|
+
private standaloneMode;
|
|
21
|
+
private enabled;
|
|
22
|
+
private _disabledReason;
|
|
23
|
+
private redis;
|
|
24
|
+
constructor(app: any, options?: {
|
|
25
|
+
standaloneMode?: boolean;
|
|
26
|
+
enabled?: boolean;
|
|
27
|
+
disabledReason?: string;
|
|
28
|
+
});
|
|
29
|
+
get isLeader(): boolean;
|
|
30
|
+
get leaderId(): string;
|
|
31
|
+
get disabledReason(): string;
|
|
32
|
+
/**
|
|
33
|
+
* Initialize Redis client. Must be called after Redis is connected.
|
|
34
|
+
*/
|
|
35
|
+
init(): Promise<boolean>;
|
|
36
|
+
/**
|
|
37
|
+
* Attempt to become the orchestrator leader.
|
|
38
|
+
* If successful, starts a renewal loop.
|
|
39
|
+
* If not, starts a retry loop.
|
|
40
|
+
*/
|
|
41
|
+
tryBecomeLeader(): Promise<boolean>;
|
|
42
|
+
/**
|
|
43
|
+
* Gracefully release leadership.
|
|
44
|
+
*/
|
|
45
|
+
release(): Promise<void>;
|
|
46
|
+
private startRenewal;
|
|
47
|
+
private startRetry;
|
|
48
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestrator Adapter Interface
|
|
3
|
+
*
|
|
4
|
+
* Abstraction layer for container runtimes (Docker, K8s, Swarm).
|
|
5
|
+
* Each adapter implements this interface so the plugin logic is runtime-agnostic.
|
|
6
|
+
*/
|
|
7
|
+
export interface ContainerInfo {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
status: 'running' | 'stopped' | 'pending' | 'error' | 'creating' | 'exited';
|
|
11
|
+
image: string;
|
|
12
|
+
createdAt: Date;
|
|
13
|
+
cpu?: number;
|
|
14
|
+
memory?: number;
|
|
15
|
+
labels?: Record<string, string>;
|
|
16
|
+
}
|
|
17
|
+
export interface ScaleResult {
|
|
18
|
+
previousReplicas: number;
|
|
19
|
+
currentReplicas: number;
|
|
20
|
+
containersCreated?: string[];
|
|
21
|
+
containersRemoved?: string[];
|
|
22
|
+
}
|
|
23
|
+
export interface ContainerStats {
|
|
24
|
+
cpu: number;
|
|
25
|
+
memory: number;
|
|
26
|
+
memoryLimit: number;
|
|
27
|
+
networkRx: number;
|
|
28
|
+
networkTx: number;
|
|
29
|
+
}
|
|
30
|
+
export interface StackConfig {
|
|
31
|
+
id?: number;
|
|
32
|
+
name: string;
|
|
33
|
+
adapter: 'docker' | 'kubernetes';
|
|
34
|
+
image: string;
|
|
35
|
+
command?: string;
|
|
36
|
+
envVars?: Record<string, string>;
|
|
37
|
+
volumes?: string[];
|
|
38
|
+
networks?: string[];
|
|
39
|
+
resourceLimits?: {
|
|
40
|
+
memory?: string;
|
|
41
|
+
cpu?: string;
|
|
42
|
+
};
|
|
43
|
+
replicas: number;
|
|
44
|
+
desiredReplicas: number;
|
|
45
|
+
enabled: boolean;
|
|
46
|
+
namespace?: string;
|
|
47
|
+
deploymentName?: string;
|
|
48
|
+
serviceAccountName?: string;
|
|
49
|
+
imagePullPolicy?: string;
|
|
50
|
+
k8sContainerName?: string;
|
|
51
|
+
k8sEnv?: Record<string, any>[];
|
|
52
|
+
k8sEnvFrom?: Record<string, any>[];
|
|
53
|
+
k8sVolumeMounts?: Record<string, any>[];
|
|
54
|
+
k8sVolumes?: Record<string, any>[];
|
|
55
|
+
networkMode?: string;
|
|
56
|
+
restartPolicy?: string;
|
|
57
|
+
}
|
|
58
|
+
export interface IOrchestratorAdapter {
|
|
59
|
+
/** Human-readable adapter name */
|
|
60
|
+
readonly name: string;
|
|
61
|
+
/** Test connectivity to the runtime */
|
|
62
|
+
ping(): Promise<boolean>;
|
|
63
|
+
/** List containers managed by a stack */
|
|
64
|
+
listContainers(stack: StackConfig): Promise<ContainerInfo[]>;
|
|
65
|
+
/** Verify that a runtime container/pod belongs to the requested stack */
|
|
66
|
+
assertManagedByStack(stack: StackConfig, containerId: string): Promise<void>;
|
|
67
|
+
/** Scale stack to N replicas */
|
|
68
|
+
scale(stack: StackConfig, replicas: number): Promise<ScaleResult>;
|
|
69
|
+
/** Start a stopped container */
|
|
70
|
+
startContainer(containerId: string): Promise<void>;
|
|
71
|
+
/** Gracefully stop a running container */
|
|
72
|
+
stopContainer(containerId: string, timeoutSecs?: number): Promise<void>;
|
|
73
|
+
/** Remove/delete a container (force) */
|
|
74
|
+
removeContainer(containerId: string): Promise<void>;
|
|
75
|
+
/** Get real-time resource stats for a container */
|
|
76
|
+
getStats(containerId: string): Promise<ContainerStats>;
|
|
77
|
+
/** Get tail logs from a container */
|
|
78
|
+
getLogs(containerId: string, tail?: number): Promise<string>;
|
|
79
|
+
/** List available networks (if supported by adapter) */
|
|
80
|
+
listNetworks?(): Promise<{
|
|
81
|
+
id: string;
|
|
82
|
+
name: string;
|
|
83
|
+
}[]>;
|
|
84
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Plugin } from '@nocobase/server';
|
|
2
|
+
import { RedisNodeRegistry } from './adapters/redis-node-registry';
|
|
3
|
+
import type { IOrchestratorAdapter } from './orchestrator/types';
|
|
4
|
+
import { LeaderElection } from './orchestrator/leader-election';
|
|
5
|
+
export declare class PluginClusterManagerServer extends Plugin {
|
|
6
|
+
nodeRegistry: RedisNodeRegistry;
|
|
7
|
+
orchestrator: IOrchestratorAdapter | null;
|
|
8
|
+
leaderElection: LeaderElection | null;
|
|
9
|
+
beforeLoad(): Promise<void>;
|
|
10
|
+
load(): Promise<void>;
|
|
11
|
+
private registerPubSubAdapter;
|
|
12
|
+
/**
|
|
13
|
+
* Initialize the Container Orchestrator subsystem.
|
|
14
|
+
* Config is loaded from DB (orchestratorSettings collection) first,
|
|
15
|
+
* then falls back to ORCHESTRATOR_ADAPTER env var.
|
|
16
|
+
* This allows manual configuration via the NocoBase admin UI.
|
|
17
|
+
*/
|
|
18
|
+
private initOrchestrator;
|
|
19
|
+
/**
|
|
20
|
+
* Connect (or reconnect) the orchestrator adapter based on settings.
|
|
21
|
+
* Can be called at startup or when user saves new settings via UI.
|
|
22
|
+
*/
|
|
23
|
+
connectAdapter(settings: any): Promise<boolean>;
|
|
24
|
+
private isWorkerOnlyNode;
|
|
25
|
+
}
|
|
26
|
+
export default PluginClusterManagerServer;
|
package/dist/server/plugin.js
CHANGED
|
@@ -71,6 +71,15 @@ class PluginClusterManagerServer extends import_server.Plugin {
|
|
|
71
71
|
await this.db.import({ directory: import_path.default.resolve(__dirname, "collections") });
|
|
72
72
|
}
|
|
73
73
|
async load() {
|
|
74
|
+
this.db.extendCollection({
|
|
75
|
+
name: "attachments",
|
|
76
|
+
fields: [
|
|
77
|
+
{
|
|
78
|
+
type: "bigInt",
|
|
79
|
+
name: "createdById"
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
});
|
|
74
83
|
this.nodeRegistry = new import_redis_node_registry.RedisNodeRegistry(this.app);
|
|
75
84
|
this.app.on("afterStart", () => {
|
|
76
85
|
var _a;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a universally unique identifier for this specific Node.js process.
|
|
3
|
+
* Combines app name, worker mode, hostname, port, and PID to ensure uniqueness
|
|
4
|
+
* even when multiple workers run on the exact same host.
|
|
5
|
+
*/
|
|
6
|
+
export declare function getLocalNodeId(app: any): string;
|