plugin-cluster-manager 1.1.7 → 1.1.11

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 (108) hide show
  1. package/client.js +1 -0
  2. package/dist/client/AclCacheManager.d.ts +2 -0
  3. package/dist/client/CacheMonitor.d.ts +2 -0
  4. package/dist/client/ClusterManagerLayout.d.ts +2 -0
  5. package/dist/client/ClusterNodes.d.ts +2 -0
  6. package/dist/client/ContainerOrchestrator.d.ts +2 -0
  7. package/dist/client/Doctor.d.ts +2 -0
  8. package/dist/client/EventQueueMonitor.d.ts +2 -0
  9. package/dist/client/LockMonitor.d.ts +2 -0
  10. package/dist/client/NginxCacheManager.d.ts +2 -0
  11. package/dist/client/PackageInstaller.d.ts +2 -0
  12. package/dist/client/PluginOperations.d.ts +2 -0
  13. package/dist/client/RedisMonitor.d.ts +2 -0
  14. package/dist/client/TaskManager.d.ts +2 -0
  15. package/dist/client/WorkflowExecutions.d.ts +2 -0
  16. package/dist/client/index.d.ts +5 -0
  17. package/dist/client/index.js +1 -1
  18. package/dist/client/utils/clientSafeCache.d.ts +3 -0
  19. package/dist/client/utils/requestDedupInterceptor.d.ts +2 -0
  20. package/dist/client/utils.d.ts +12 -0
  21. package/dist/externalVersion.js +5 -5
  22. package/dist/index.d.ts +2 -0
  23. package/dist/locale/en-US.json +97 -1
  24. package/dist/locale/vi-VN.json +98 -1
  25. package/dist/locale/zh-CN.json +98 -1
  26. package/dist/server/actions/acl-cache.d.ts +53 -0
  27. package/dist/server/actions/acl-cache.js +1 -1
  28. package/dist/server/actions/cache-monitor.d.ts +33 -0
  29. package/dist/server/actions/cache-monitor.js +301 -0
  30. package/dist/server/actions/cluster-nodes.d.ts +64 -0
  31. package/dist/server/actions/cluster-nodes.js +394 -10
  32. package/dist/server/actions/doctor.d.ts +82 -0
  33. package/dist/server/actions/doctor.js +1250 -0
  34. package/dist/server/actions/event-queue-monitor.d.ts +13 -0
  35. package/dist/server/actions/lock-monitor.d.ts +19 -0
  36. package/dist/server/actions/orchestrator.d.ts +58 -0
  37. package/dist/server/actions/package-manager.d.ts +6 -0
  38. package/dist/server/actions/plugin-operations.d.ts +6 -0
  39. package/dist/server/actions/redis-monitor.d.ts +12 -0
  40. package/dist/server/actions/tasks.d.ts +7 -0
  41. package/dist/server/actions/workflow-executions.d.ts +7 -0
  42. package/dist/server/adapters/redis-lock-adapter.d.ts +15 -0
  43. package/dist/server/adapters/redis-node-registry.d.ts +12 -0
  44. package/dist/server/adapters/redis-pubsub-adapter.d.ts +16 -0
  45. package/dist/server/collections/app.d.ts +8 -0
  46. package/dist/server/collections/cluster-manager-acl-cache.d.ts +22 -0
  47. package/dist/server/collections/cluster-manager-cache-mgr.d.ts +22 -0
  48. package/dist/server/collections/cluster-manager-cluster.d.ts +22 -0
  49. package/dist/server/collections/cluster-manager-doctor-runs.d.ts +3 -0
  50. package/dist/server/collections/cluster-manager-doctor-runs.js +52 -0
  51. package/dist/server/collections/cluster-manager-doctor.d.ts +18 -0
  52. package/dist/server/collections/cluster-manager-doctor.js +44 -0
  53. package/dist/server/collections/cluster-manager-lock.d.ts +22 -0
  54. package/dist/server/collections/cluster-manager-plugins.d.ts +18 -0
  55. package/dist/server/collections/cluster-manager-queue.d.ts +22 -0
  56. package/dist/server/collections/cluster-manager-redis.d.ts +22 -0
  57. package/dist/server/collections/cluster-manager-workflow.d.ts +22 -0
  58. package/dist/server/collections/cluster-manager.d.ts +22 -0
  59. package/dist/server/collections/orchestrator-settings.d.ts +59 -0
  60. package/dist/server/collections/orchestrator-stacks.d.ts +102 -0
  61. package/dist/server/collections/worker-orchestrator.d.ts +22 -0
  62. package/dist/server/collections/worker-packages-configs.d.ts +3 -0
  63. package/dist/server/collections/worker-packages.d.ts +22 -0
  64. package/dist/server/hooks/cacheInvalidationHooks.d.ts +1 -0
  65. package/dist/server/hooks/cacheInvalidationHooks.js +81 -0
  66. package/dist/server/index.d.ts +1 -0
  67. package/dist/server/middlewares/listMetaCacheMiddleware.d.ts +2 -0
  68. package/dist/server/middlewares/listMetaCacheMiddleware.js +79 -0
  69. package/dist/server/orchestrator/PackageManager.d.ts +39 -0
  70. package/dist/server/orchestrator/PackageManager.js +83 -27
  71. package/dist/server/orchestrator/docker-adapter.d.ts +41 -0
  72. package/dist/server/orchestrator/index.d.ts +4 -0
  73. package/dist/server/orchestrator/k8s-adapter.d.ts +50 -0
  74. package/dist/server/orchestrator/leader-election.d.ts +48 -0
  75. package/dist/server/orchestrator/types.d.ts +84 -0
  76. package/dist/server/plugin.d.ts +26 -0
  77. package/dist/server/plugin.js +70 -8
  78. package/dist/server/utils/node.d.ts +6 -0
  79. package/dist/server/utils/redis.d.ts +29 -0
  80. package/dist/server/utils/versionManager.d.ts +10 -0
  81. package/dist/server/utils/versionManager.js +91 -0
  82. package/dist/shared/packages.d.ts +23 -0
  83. package/package.json +41 -41
  84. package/server.js +1 -0
  85. package/src/client/CacheMonitor.tsx +166 -179
  86. package/src/client/ClusterManagerLayout.tsx +48 -42
  87. package/src/client/ClusterNodes.tsx +691 -418
  88. package/src/client/Doctor.tsx +559 -0
  89. package/src/client/NginxCacheManager.tsx +415 -0
  90. package/src/client/PluginOperations.tsx +234 -234
  91. package/src/client/index.tsx +22 -14
  92. package/src/client/utils/clientSafeCache.ts +41 -0
  93. package/src/client/utils/requestDedupInterceptor.ts +213 -0
  94. package/src/locale/en-US.json +97 -1
  95. package/src/locale/vi-VN.json +98 -1
  96. package/src/locale/zh-CN.json +98 -1
  97. package/src/server/__tests__/doctor.test.ts +53 -0
  98. package/src/server/actions/acl-cache.ts +272 -272
  99. package/src/server/actions/cache-monitor.ts +453 -116
  100. package/src/server/actions/cluster-nodes.ts +882 -378
  101. package/src/server/actions/doctor.ts +1540 -0
  102. package/src/server/collections/cluster-manager-doctor-runs.ts +23 -0
  103. package/src/server/collections/cluster-manager-doctor.ts +19 -0
  104. package/src/server/hooks/cacheInvalidationHooks.ts +58 -0
  105. package/src/server/middlewares/listMetaCacheMiddleware.ts +55 -0
  106. package/src/server/orchestrator/PackageManager.ts +19 -15
  107. package/src/server/plugin.ts +353 -263
  108. package/src/server/utils/versionManager.ts +69 -0
@@ -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 declare function registerCacheHooks(app: any): void;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ var __defProp = Object.defineProperty;
11
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
+ var __getOwnPropNames = Object.getOwnPropertyNames;
13
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __export = (target, all) => {
15
+ for (var name in all)
16
+ __defProp(target, name, { get: all[name], enumerable: true });
17
+ };
18
+ var __copyProps = (to, from, except, desc) => {
19
+ if (from && typeof from === "object" || typeof from === "function") {
20
+ for (let key of __getOwnPropNames(from))
21
+ if (!__hasOwnProp.call(to, key) && key !== except)
22
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
23
+ }
24
+ return to;
25
+ };
26
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
27
+ var cacheInvalidationHooks_exports = {};
28
+ __export(cacheInvalidationHooks_exports, {
29
+ registerCacheHooks: () => registerCacheHooks
30
+ });
31
+ module.exports = __toCommonJS(cacheInvalidationHooks_exports);
32
+ var import_versionManager = require("../utils/versionManager");
33
+ function registerCacheHooks(app) {
34
+ const db = app.db;
35
+ db.on("collections.afterSave", async () => {
36
+ await import_versionManager.cacheVersionManager.incrementCollectionVersion(app);
37
+ });
38
+ db.on("collections.afterDestroy", async () => {
39
+ await import_versionManager.cacheVersionManager.incrementCollectionVersion(app);
40
+ });
41
+ db.on("fields.afterSave", async () => {
42
+ await import_versionManager.cacheVersionManager.incrementCollectionVersion(app);
43
+ });
44
+ db.on("fields.afterDestroy", async () => {
45
+ await import_versionManager.cacheVersionManager.incrementCollectionVersion(app);
46
+ });
47
+ db.on("uiSchemas.afterSave", async () => {
48
+ await import_versionManager.cacheVersionManager.incrementSchemaVersion(app);
49
+ });
50
+ db.on("uiSchemas.afterDestroy", async () => {
51
+ await import_versionManager.cacheVersionManager.incrementSchemaVersion(app);
52
+ });
53
+ const invalidateRole = async (model) => {
54
+ var _a, _b;
55
+ const roleName = ((_a = model.get) == null ? void 0 : _a.call(model, "roleName")) || ((_b = model.get) == null ? void 0 : _b.call(model, "name"));
56
+ if (roleName) {
57
+ await import_versionManager.cacheVersionManager.incrementAclVersion(app, roleName);
58
+ } else {
59
+ await import_versionManager.cacheVersionManager.incrementAllAclVersions(app);
60
+ }
61
+ };
62
+ db.on("roles.afterSave", invalidateRole);
63
+ db.on("roles.afterDestroy", invalidateRole);
64
+ db.on("rolesResources.afterSave", invalidateRole);
65
+ db.on("rolesResources.afterDestroy", invalidateRole);
66
+ db.on("rolesResourcesActions.afterSave", invalidateRole);
67
+ db.on("rolesResourcesActions.afterDestroy", invalidateRole);
68
+ db.on("rolesUsers.afterSave", invalidateRole);
69
+ db.on("rolesUsers.afterDestroy", invalidateRole);
70
+ db.on("scopes.afterSave", async () => {
71
+ await import_versionManager.cacheVersionManager.incrementAllAclVersions(app);
72
+ });
73
+ db.on("scopes.afterDestroy", async () => {
74
+ await import_versionManager.cacheVersionManager.incrementAllAclVersions(app);
75
+ });
76
+ app.logger.info("[ClusterManager] Cache invalidation hooks registered successfully");
77
+ }
78
+ // Annotate the CommonJS export names for ESM import in node:
79
+ 0 && (module.exports = {
80
+ registerCacheHooks
81
+ });
@@ -0,0 +1 @@
1
+ export { default } from './plugin';
@@ -0,0 +1,2 @@
1
+ import { Context } from '@nocobase/actions';
2
+ export declare function createListMetaCacheMiddleware(app: any): (ctx: Context, next: () => Promise<void>) => Promise<void>;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ var __defProp = Object.defineProperty;
11
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
+ var __getOwnPropNames = Object.getOwnPropertyNames;
13
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __export = (target, all) => {
15
+ for (var name in all)
16
+ __defProp(target, name, { get: all[name], enumerable: true });
17
+ };
18
+ var __copyProps = (to, from, except, desc) => {
19
+ if (from && typeof from === "object" || typeof from === "function") {
20
+ for (let key of __getOwnPropNames(from))
21
+ if (!__hasOwnProp.call(to, key) && key !== except)
22
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
23
+ }
24
+ return to;
25
+ };
26
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
27
+ var listMetaCacheMiddleware_exports = {};
28
+ __export(listMetaCacheMiddleware_exports, {
29
+ createListMetaCacheMiddleware: () => createListMetaCacheMiddleware
30
+ });
31
+ module.exports = __toCommonJS(listMetaCacheMiddleware_exports);
32
+ var import_versionManager = require("../utils/versionManager");
33
+ const LIST_META_CACHE_TTL = 1e3 * 60 * 10;
34
+ function getErrorMessage(error) {
35
+ return error instanceof Error ? error.message : String(error);
36
+ }
37
+ function createListMetaCacheMiddleware(app) {
38
+ return async function listMetaCacheMiddleware(ctx, next) {
39
+ var _a, _b, _c, _d, _e, _f, _g;
40
+ const cache = app.cache;
41
+ if (!cache || ((_a = ctx.action) == null ? void 0 : _a.resourceName) !== "collections" || ((_b = ctx.action) == null ? void 0 : _b.actionName) !== "listMeta") {
42
+ return next();
43
+ }
44
+ const currentRole = ((_c = ctx.state) == null ? void 0 : _c.currentRole) || "anonymous";
45
+ const appName = ctx.headers["x-app"] || "main";
46
+ const dataSource = ctx.headers["x-data-source"] || "main";
47
+ const locale = ctx.headers["x-locale"] || ctx.headers["accept-language"] || "en-US";
48
+ let cacheKey = "";
49
+ let version = 0;
50
+ try {
51
+ version = await import_versionManager.cacheVersionManager.getCollectionVersion(app);
52
+ cacheKey = `nb:cache:${appName}:meta:v${version}:ds:${dataSource}:role:${currentRole}:lang:${locale}`;
53
+ const cached = await cache.get(cacheKey);
54
+ if (cached !== void 0 && cached !== null) {
55
+ ctx.body = typeof cached === "string" ? JSON.parse(cached) : cached;
56
+ (_d = ctx.set) == null ? void 0 : _d.call(ctx, "X-Cache", "HIT");
57
+ (_e = ctx.set) == null ? void 0 : _e.call(ctx, "X-Collection-Version", String(version));
58
+ return;
59
+ }
60
+ } catch (err) {
61
+ app.logger.warn(`[ClusterManager] listMeta cache read skipped: ${getErrorMessage(err)}`);
62
+ }
63
+ await next();
64
+ if (ctx.status === 200 && ctx.body && cacheKey) {
65
+ try {
66
+ const valueToCache = typeof ctx.body === "string" ? ctx.body : JSON.stringify(ctx.body);
67
+ await cache.set(cacheKey, valueToCache, LIST_META_CACHE_TTL);
68
+ (_f = ctx.set) == null ? void 0 : _f.call(ctx, "X-Cache", "MISS");
69
+ (_g = ctx.set) == null ? void 0 : _g.call(ctx, "X-Collection-Version", String(version));
70
+ } catch (err) {
71
+ app.logger.warn(`[ClusterManager] listMeta cache write skipped: ${getErrorMessage(err)}`);
72
+ }
73
+ }
74
+ };
75
+ }
76
+ // Annotate the CommonJS export names for ESM import in node:
77
+ 0 && (module.exports = {
78
+ createListMetaCacheMiddleware
79
+ });
@@ -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 {};
@@ -41,9 +41,10 @@ __export(PackageManager_exports, {
41
41
  module.exports = __toCommonJS(PackageManager_exports);
42
42
  var import_child_process = require("child_process");
43
43
  var import_redis = require("../utils/redis");
44
+ var import_node = require("../utils/node");
44
45
  var import_fs = require("fs");
45
46
  var import_path = __toESM(require("path"));
46
- const SAFE_PKG_RE = /^[a-zA-Z0-9_\-\.@\/\[\]]+$/;
47
+ const SAFE_PKG_RE = /^(?:[a-zA-Z0-9_.@/-]|\[|\])+$/;
47
48
  const INSTALL_CHANNEL = "cluster-manager.install-packages";
48
49
  function sanitizePkg(name) {
49
50
  if (typeof name !== "string") {
@@ -99,6 +100,35 @@ function formatCommand(command, args) {
99
100
  return /^[a-zA-Z0-9_./:@=-]+$/.test(value) ? value : JSON.stringify(value);
100
101
  }).join(" ");
101
102
  }
103
+ function parseOsRelease(content) {
104
+ const values = {};
105
+ for (const line of content.split("\n")) {
106
+ const match = line.match(/^([A-Z0-9_]+)=(.*)$/);
107
+ if (!match) continue;
108
+ values[match[1]] = match[2].replace(/^"|"$/g, "");
109
+ }
110
+ return values;
111
+ }
112
+ function normalizeMirrorUrl(value) {
113
+ return value.endsWith("/") ? value : `${value}/`;
114
+ }
115
+ function isInternalCacheMirror(url) {
116
+ return url.hostname === "nginx-cache-registry";
117
+ }
118
+ function resolveAptMirrorForOs(aptMirrorUrl, osInfo, logs) {
119
+ const url = new URL(normalizeMirrorUrl(aptMirrorUrl));
120
+ const original = url.toString();
121
+ if (isInternalCacheMirror(url) && osInfo.id === "debian" && url.pathname === "/ubuntu/") {
122
+ url.pathname = "/debian/";
123
+ } else if (isInternalCacheMirror(url) && osInfo.id === "ubuntu" && url.pathname === "/debian/") {
124
+ url.pathname = "/ubuntu/";
125
+ }
126
+ const resolved = url.toString();
127
+ if (resolved !== original) {
128
+ logs.push(`Adjusted APT mirror for ${osInfo.id}: ${redactUrl(resolved)}`);
129
+ }
130
+ return resolved;
131
+ }
102
132
  class PackageManager {
103
133
  constructor(app) {
104
134
  this.app = app;
@@ -143,7 +173,7 @@ class PackageManager {
143
173
  if (registryConfig.aptMirrorUrl) {
144
174
  const aptMirrorUrl = sanitizeHttpUrl(registryConfig.aptMirrorUrl, "APT mirror URL");
145
175
  logs.push(`Applying APT mirror: ${redactUrl(aptMirrorUrl)}`);
146
- await this.configureAptMirror(aptMirrorUrl);
176
+ await this.configureAptMirror(aptMirrorUrl, logs);
147
177
  }
148
178
  await this.updateInstallStatus("running", 20, "Installing APT packages...", logs);
149
179
  await this.runCommand("apt-get", ["update", "-qq"], "Updating APT package index...", logs, 12e5);
@@ -245,11 +275,11 @@ ${logs.join("\n")}`);
245
275
  let stdout = "";
246
276
  let stderr = "";
247
277
  let settled = false;
248
- let timer;
278
+ const state = {};
249
279
  const finish = (error) => {
250
280
  if (settled) return;
251
281
  settled = true;
252
- clearTimeout(timer);
282
+ if (state.timer) clearTimeout(state.timer);
253
283
  if (stdout) logs.push(stdout.slice(0, 500));
254
284
  if (stderr) logs.push(`WARN: ${stderr.slice(0, 300)}`);
255
285
  if (error) {
@@ -259,7 +289,7 @@ ${logs.join("\n")}`);
259
289
  resolve();
260
290
  }
261
291
  };
262
- timer = setTimeout(() => {
292
+ state.timer = setTimeout(() => {
263
293
  child.kill("SIGTERM");
264
294
  finish(new Error(`${command} timed out after ${timeoutMs}ms`));
265
295
  }, timeoutMs);
@@ -312,14 +342,14 @@ ${logs.join("\n")}`);
312
342
  const child = (0, import_child_process.spawn)(command, args, { stdio: ["ignore", "pipe", "ignore"] });
313
343
  let stdout = "";
314
344
  let settled = false;
315
- let timer;
345
+ const state = {};
316
346
  const finish = (ok) => {
317
347
  if (settled) return;
318
348
  settled = true;
319
- clearTimeout(timer);
349
+ if (state.timer) clearTimeout(state.timer);
320
350
  resolve(ok);
321
351
  };
322
- timer = setTimeout(() => {
352
+ state.timer = setTimeout(() => {
323
353
  child.kill("SIGTERM");
324
354
  finish(false);
325
355
  }, timeoutMs);
@@ -330,7 +360,31 @@ ${logs.join("\n")}`);
330
360
  child.on("close", (code) => finish(code === 0 && (isSuccess ? isSuccess(stdout) : true)));
331
361
  });
332
362
  }
333
- async configureAptMirror(aptMirrorUrl) {
363
+ async getAptOsInfo() {
364
+ const values = parseOsRelease(await import_fs.promises.readFile("/etc/os-release", "utf8"));
365
+ const id = (values.ID || "").toLowerCase();
366
+ const codename = values.VERSION_CODENAME || values.UBUNTU_CODENAME;
367
+ if (!id || !codename) {
368
+ throw new Error("Cannot detect OS ID/version codename from /etc/os-release for APT mirror configuration.");
369
+ }
370
+ return { id, codename };
371
+ }
372
+ buildAptSources(aptMirrorUrl, osInfo) {
373
+ const mirror = normalizeMirrorUrl(aptMirrorUrl);
374
+ if (osInfo.id === "ubuntu") {
375
+ return `deb ${mirror} ${osInfo.codename} main universe restricted multiverse
376
+ `;
377
+ }
378
+ if (osInfo.id === "debian") {
379
+ return `deb ${mirror} ${osInfo.codename} main contrib non-free non-free-firmware
380
+ `;
381
+ }
382
+ return `deb ${mirror} ${osInfo.codename} main
383
+ `;
384
+ }
385
+ async configureAptMirror(aptMirrorUrl, logs) {
386
+ const osInfo = await this.getAptOsInfo();
387
+ const resolvedMirrorUrl = resolveAptMirrorForOs(aptMirrorUrl, osInfo, logs);
334
388
  const backupDir = "/etc/apt/sources.list.d.bak";
335
389
  await import_fs.promises.mkdir(backupDir, { recursive: true });
336
390
  await this.moveIfExists("/etc/apt/sources.list", import_path.default.join(backupDir, `sources.list.${Date.now()}.bak`));
@@ -342,8 +396,7 @@ ${logs.join("\n")}`);
342
396
  }
343
397
  } catch {
344
398
  }
345
- await import_fs.promises.writeFile("/etc/apt/sources.list", `deb ${aptMirrorUrl} bookworm main
346
- `, "utf8");
399
+ await import_fs.promises.writeFile("/etc/apt/sources.list", this.buildAptSources(resolvedMirrorUrl, osInfo), "utf8");
347
400
  }
348
401
  async moveIfExists(from, to) {
349
402
  try {
@@ -356,22 +409,25 @@ ${logs.join("\n")}`);
356
409
  const redisClient = (0, import_redis.getRedisClient)(this.app);
357
410
  const podName = process.env.POD_NAME || require("os").hostname();
358
411
  if (redisClient) {
359
- const key = `orchestrator:pkg-status:${podName}`;
360
- await redisClient.sendCommand([
361
- "SET",
362
- key,
363
- JSON.stringify({
364
- initStatus,
365
- initProgressPercent,
366
- initProgressLog,
367
- lastInitAt: /* @__PURE__ */ new Date(),
368
- lastInitLog: logs.join("\n"),
369
- ...extraValues
370
- }),
371
- "EX",
372
- "86400"
373
- // expire after 1 day
374
- ]);
412
+ const statusPayload = JSON.stringify({
413
+ initStatus,
414
+ initProgressPercent,
415
+ initProgressLog,
416
+ lastInitAt: /* @__PURE__ */ new Date(),
417
+ lastInitLog: logs.join("\n"),
418
+ ...extraValues
419
+ });
420
+ const keys = [`orchestrator:pkg-status:${podName}`, `cluster-manager:pkg-status:${(0, import_node.getLocalNodeId)(this.app)}`];
421
+ for (const key of keys) {
422
+ await redisClient.sendCommand([
423
+ "SET",
424
+ key,
425
+ statusPayload,
426
+ "EX",
427
+ "86400"
428
+ // expire after 1 day
429
+ ]);
430
+ }
375
431
  }
376
432
  const repo = this.app.db.getRepository("workerPackagesConfigs");
377
433
  let config = await repo.findOne();
@@ -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
+ }