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.
Files changed (61) 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/utils.d.ts +12 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/server/actions/acl-cache.d.ts +53 -0
  17. package/dist/server/actions/acl-cache.js +1 -1
  18. package/dist/server/actions/cache-monitor.d.ts +23 -0
  19. package/dist/server/actions/cluster-nodes.d.ts +49 -0
  20. package/dist/server/actions/event-queue-monitor.d.ts +13 -0
  21. package/dist/server/actions/lock-monitor.d.ts +19 -0
  22. package/dist/server/actions/orchestrator.d.ts +58 -0
  23. package/dist/server/actions/package-manager.d.ts +6 -0
  24. package/dist/server/actions/plugin-operations.d.ts +6 -0
  25. package/dist/server/actions/redis-monitor.d.ts +12 -0
  26. package/dist/server/actions/tasks.d.ts +7 -0
  27. package/dist/server/actions/workflow-executions.d.ts +7 -0
  28. package/dist/server/adapters/redis-lock-adapter.d.ts +15 -0
  29. package/dist/server/adapters/redis-node-registry.d.ts +12 -0
  30. package/dist/server/adapters/redis-pubsub-adapter.d.ts +16 -0
  31. package/dist/server/collections/app.d.ts +8 -0
  32. package/dist/server/collections/cluster-manager-acl-cache.d.ts +22 -0
  33. package/dist/server/collections/cluster-manager-cache-mgr.d.ts +22 -0
  34. package/dist/server/collections/cluster-manager-cluster.d.ts +22 -0
  35. package/dist/server/collections/cluster-manager-lock.d.ts +22 -0
  36. package/dist/server/collections/cluster-manager-plugins.d.ts +18 -0
  37. package/dist/server/collections/cluster-manager-queue.d.ts +22 -0
  38. package/dist/server/collections/cluster-manager-redis.d.ts +22 -0
  39. package/dist/server/collections/cluster-manager-workflow.d.ts +22 -0
  40. package/dist/server/collections/cluster-manager.d.ts +22 -0
  41. package/dist/server/collections/orchestrator-settings.d.ts +59 -0
  42. package/dist/server/collections/orchestrator-stacks.d.ts +102 -0
  43. package/dist/server/collections/worker-orchestrator.d.ts +22 -0
  44. package/dist/server/collections/worker-packages-configs.d.ts +3 -0
  45. package/dist/server/collections/worker-packages.d.ts +22 -0
  46. package/dist/server/index.d.ts +1 -0
  47. package/dist/server/orchestrator/PackageManager.d.ts +39 -0
  48. package/dist/server/orchestrator/PackageManager.js +63 -11
  49. package/dist/server/orchestrator/docker-adapter.d.ts +41 -0
  50. package/dist/server/orchestrator/index.d.ts +4 -0
  51. package/dist/server/orchestrator/k8s-adapter.d.ts +50 -0
  52. package/dist/server/orchestrator/leader-election.d.ts +48 -0
  53. package/dist/server/orchestrator/types.d.ts +84 -0
  54. package/dist/server/plugin.d.ts +26 -0
  55. package/dist/server/plugin.js +9 -0
  56. package/dist/server/utils/node.d.ts +6 -0
  57. package/dist/server/utils/redis.d.ts +29 -0
  58. package/dist/shared/packages.d.ts +23 -0
  59. package/package.json +1 -1
  60. package/src/server/actions/acl-cache.ts +2 -2
  61. 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,3 @@
1
+ import { CollectionOptions } from '@nocobase/database';
2
+ declare const _default: CollectionOptions;
3
+ 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
- let timer;
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
- let timer;
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 configureAptMirror(aptMirrorUrl) {
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", `deb ${aptMirrorUrl} bookworm main
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,4 @@
1
+ export { IOrchestratorAdapter, ContainerInfo, ScaleResult, ContainerStats, StackConfig } from './types';
2
+ export { DockerAdapter } from './docker-adapter';
3
+ export { K8sAdapter } from './k8s-adapter';
4
+ export { LeaderElection } from './leader-election';
@@ -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;
@@ -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;