llng-mcp 0.1.0
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/.github/workflows/ci.yml +77 -0
- package/.prettierrc +7 -0
- package/LICENSE +661 -0
- package/README.md +502 -0
- package/dist/__tests__/api-transport.test.d.ts +1 -0
- package/dist/__tests__/api-transport.test.js +577 -0
- package/dist/__tests__/api-transport.test.js.map +1 -0
- package/dist/__tests__/config.test.d.ts +1 -0
- package/dist/__tests__/config.test.js +472 -0
- package/dist/__tests__/config.test.js.map +1 -0
- package/dist/__tests__/integration/api-mode.test.d.ts +1 -0
- package/dist/__tests__/integration/api-mode.test.js +199 -0
- package/dist/__tests__/integration/api-mode.test.js.map +1 -0
- package/dist/__tests__/integration/oidc-rp.test.d.ts +1 -0
- package/dist/__tests__/integration/oidc-rp.test.js +120 -0
- package/dist/__tests__/integration/oidc-rp.test.js.map +1 -0
- package/dist/__tests__/integration/ssh-mode.test.d.ts +1 -0
- package/dist/__tests__/integration/ssh-mode.test.js +101 -0
- package/dist/__tests__/integration/ssh-mode.test.js.map +1 -0
- package/dist/__tests__/k8s-transport.test.d.ts +1 -0
- package/dist/__tests__/k8s-transport.test.js +254 -0
- package/dist/__tests__/k8s-transport.test.js.map +1 -0
- package/dist/__tests__/oidc-tools.test.d.ts +1 -0
- package/dist/__tests__/oidc-tools.test.js +457 -0
- package/dist/__tests__/oidc-tools.test.js.map +1 -0
- package/dist/__tests__/registry.test.d.ts +1 -0
- package/dist/__tests__/registry.test.js +96 -0
- package/dist/__tests__/registry.test.js.map +1 -0
- package/dist/__tests__/ssh-transport.test.d.ts +1 -0
- package/dist/__tests__/ssh-transport.test.js +618 -0
- package/dist/__tests__/ssh-transport.test.js.map +1 -0
- package/dist/__tests__/tools.test.d.ts +1 -0
- package/dist/__tests__/tools.test.js +525 -0
- package/dist/__tests__/tools.test.js.map +1 -0
- package/dist/config.d.ts +65 -0
- package/dist/config.js +506 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/resources/documentation.d.ts +5 -0
- package/dist/resources/documentation.js +56 -0
- package/dist/resources/documentation.js.map +1 -0
- package/dist/tools/cli-utilities.d.ts +3 -0
- package/dist/tools/cli-utilities.js +187 -0
- package/dist/tools/cli-utilities.js.map +1 -0
- package/dist/tools/config.d.ts +6 -0
- package/dist/tools/config.js +326 -0
- package/dist/tools/config.js.map +1 -0
- package/dist/tools/consents.d.ts +3 -0
- package/dist/tools/consents.js +39 -0
- package/dist/tools/consents.js.map +1 -0
- package/dist/tools/instances.d.ts +3 -0
- package/dist/tools/instances.js +14 -0
- package/dist/tools/instances.js.map +1 -0
- package/dist/tools/oidc-rp.d.ts +6 -0
- package/dist/tools/oidc-rp.js +246 -0
- package/dist/tools/oidc-rp.js.map +1 -0
- package/dist/tools/oidc.d.ts +3 -0
- package/dist/tools/oidc.js +343 -0
- package/dist/tools/oidc.js.map +1 -0
- package/dist/tools/secondfactors.d.ts +3 -0
- package/dist/tools/secondfactors.js +62 -0
- package/dist/tools/secondfactors.js.map +1 -0
- package/dist/tools/sessions.d.ts +6 -0
- package/dist/tools/sessions.js +300 -0
- package/dist/tools/sessions.js.map +1 -0
- package/dist/transport/api.d.ts +35 -0
- package/dist/transport/api.js +327 -0
- package/dist/transport/api.js.map +1 -0
- package/dist/transport/interface.d.ts +50 -0
- package/dist/transport/interface.js +2 -0
- package/dist/transport/interface.js.map +1 -0
- package/dist/transport/k8s.d.ts +41 -0
- package/dist/transport/k8s.js +303 -0
- package/dist/transport/k8s.js.map +1 -0
- package/dist/transport/registry.d.ts +20 -0
- package/dist/transport/registry.js +91 -0
- package/dist/transport/registry.js.map +1 -0
- package/dist/transport/ssh.d.ts +37 -0
- package/dist/transport/ssh.js +353 -0
- package/dist/transport/ssh.js.map +1 -0
- package/docker-compose.test.yml +16 -0
- package/eslint.config.js +21 -0
- package/package.json +38 -0
- package/src/__tests__/api-transport.test.ts +746 -0
- package/src/__tests__/config.test.ts +587 -0
- package/src/__tests__/integration/api-mode.test.ts +229 -0
- package/src/__tests__/integration/oidc-rp.test.ts +138 -0
- package/src/__tests__/integration/ssh-mode.test.ts +113 -0
- package/src/__tests__/k8s-transport.test.ts +342 -0
- package/src/__tests__/oidc-tools.test.ts +554 -0
- package/src/__tests__/registry.test.ts +110 -0
- package/src/__tests__/ssh-transport.test.ts +805 -0
- package/src/__tests__/tools.test.ts +735 -0
- package/src/config.ts +605 -0
- package/src/index.ts +48 -0
- package/src/resources/documentation.ts +65 -0
- package/src/tools/cli-utilities.ts +207 -0
- package/src/tools/config.ts +382 -0
- package/src/tools/consents.ts +50 -0
- package/src/tools/instances.ts +21 -0
- package/src/tools/oidc-rp.ts +299 -0
- package/src/tools/oidc.ts +434 -0
- package/src/tools/secondfactors.ts +78 -0
- package/src/tools/sessions.ts +342 -0
- package/src/transport/api.ts +429 -0
- package/src/transport/interface.ts +58 -0
- package/src/transport/k8s.ts +367 -0
- package/src/transport/registry.ts +105 -0
- package/src/transport/ssh.ts +430 -0
- package/tsconfig.json +16 -0
- package/vitest.config.ts +8 -0
- package/vitest.integration.config.ts +9 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { K8sConfig, resolvePaths } from "../config.js";
|
|
3
|
+
import {
|
|
4
|
+
ILlngTransport,
|
|
5
|
+
SessionFilter,
|
|
6
|
+
ConfigInfo,
|
|
7
|
+
SessionGetOptions,
|
|
8
|
+
SessionDeleteOptions,
|
|
9
|
+
} from "./interface.js";
|
|
10
|
+
|
|
11
|
+
export class K8sTransport implements ILlngTransport {
|
|
12
|
+
private paths: { cliPath: string; sessionsPath: string; configEditorPath: string };
|
|
13
|
+
private cachedPodName: string | null = null;
|
|
14
|
+
|
|
15
|
+
constructor(private config: K8sConfig) {
|
|
16
|
+
this.paths = resolvePaths(config.binPrefix);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private async resolvePod(): Promise<string> {
|
|
20
|
+
if (this.cachedPodName) {
|
|
21
|
+
return this.cachedPodName;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!this.config.namespace) {
|
|
25
|
+
throw new Error("K8s namespace is required but not configured");
|
|
26
|
+
}
|
|
27
|
+
if (!this.config.deployment && !this.config.podSelector) {
|
|
28
|
+
throw new Error("K8s deployment or podSelector is required but not configured");
|
|
29
|
+
}
|
|
30
|
+
const selector = this.config.podSelector || `app.kubernetes.io/name=${this.config.deployment}`;
|
|
31
|
+
const args = ["get", "pods", "-l", selector, "-o", "jsonpath={.items[0].metadata.name}"];
|
|
32
|
+
|
|
33
|
+
if (this.config.context) {
|
|
34
|
+
args.unshift("--context", this.config.context);
|
|
35
|
+
}
|
|
36
|
+
args.unshift("-n", this.config.namespace!);
|
|
37
|
+
|
|
38
|
+
const podName = await this.kubectl(args);
|
|
39
|
+
if (!podName || podName === "{}" || podName.trim() === "") {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`No pod found for selector '${selector}' in namespace '${this.config.namespace}'`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this.cachedPodName = podName.trim();
|
|
46
|
+
return this.cachedPodName;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private async kubectl(args: string[]): Promise<string> {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const proc = spawn("kubectl", args);
|
|
52
|
+
let stdout = "";
|
|
53
|
+
|
|
54
|
+
proc.stdout.on("data", (data) => {
|
|
55
|
+
stdout += data.toString();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
proc.stderr.on("data", () => {
|
|
59
|
+
// stderr consumed for security
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
proc.on("close", (code) => {
|
|
63
|
+
if (code !== 0) {
|
|
64
|
+
reject(new Error(`kubectl command failed with exit code ${code}`));
|
|
65
|
+
} else {
|
|
66
|
+
resolve(stdout);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
proc.on("error", () => {
|
|
71
|
+
reject(new Error("kubectl command execution failed"));
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private async kubectlWithStdin(args: string[], input: string): Promise<string> {
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
const proc = spawn("kubectl", args);
|
|
79
|
+
let stdout = "";
|
|
80
|
+
|
|
81
|
+
proc.stdout.on("data", (data) => {
|
|
82
|
+
stdout += data.toString();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
proc.stderr.on("data", () => {
|
|
86
|
+
// stderr consumed for security
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
proc.on("close", (code) => {
|
|
90
|
+
if (code !== 0) {
|
|
91
|
+
reject(new Error(`kubectl command failed with exit code ${code}`));
|
|
92
|
+
} else {
|
|
93
|
+
resolve(stdout);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
proc.on("error", () => {
|
|
98
|
+
reject(new Error("kubectl command execution failed"));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
proc.stdin.write(input);
|
|
102
|
+
proc.stdin.end();
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private buildExecArgs(podName: string, command: string[]): string[] {
|
|
107
|
+
const args: string[] = [];
|
|
108
|
+
|
|
109
|
+
if (this.config.context) {
|
|
110
|
+
args.push("--context", this.config.context);
|
|
111
|
+
}
|
|
112
|
+
args.push("-n", this.config.namespace!);
|
|
113
|
+
args.push("exec", podName);
|
|
114
|
+
|
|
115
|
+
if (this.config.container) {
|
|
116
|
+
args.push("-c", this.config.container);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
args.push("--", ...command);
|
|
120
|
+
return args;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private buildExecStdinArgs(podName: string, command: string[]): string[] {
|
|
124
|
+
const args: string[] = [];
|
|
125
|
+
|
|
126
|
+
if (this.config.context) {
|
|
127
|
+
args.push("--context", this.config.context);
|
|
128
|
+
}
|
|
129
|
+
args.push("-n", this.config.namespace!);
|
|
130
|
+
args.push("exec", "-i", podName);
|
|
131
|
+
|
|
132
|
+
if (this.config.container) {
|
|
133
|
+
args.push("-c", this.config.container);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
args.push("--", ...command);
|
|
137
|
+
return args;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private async exec(command: string[]): Promise<string> {
|
|
141
|
+
const podName = await this.resolvePod();
|
|
142
|
+
const args = this.buildExecArgs(podName, command);
|
|
143
|
+
return this.kubectl(args);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private async execWithStdin(command: string[], input: string): Promise<string> {
|
|
147
|
+
const podName = await this.resolvePod();
|
|
148
|
+
const args = this.buildExecStdinArgs(podName, command);
|
|
149
|
+
return this.kubectlWithStdin(args, input);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private async execCli(subArgs: string[]): Promise<string> {
|
|
153
|
+
return this.exec([this.paths.cliPath, ...subArgs]);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private async execSessions(subArgs: string[]): Promise<string> {
|
|
157
|
+
return this.exec([this.paths.sessionsPath, ...subArgs]);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private pushSessionGetOptions(args: string[], options?: SessionGetOptions): void {
|
|
161
|
+
if (!options) return;
|
|
162
|
+
if (options.persistent) {
|
|
163
|
+
args.push("--persistent");
|
|
164
|
+
}
|
|
165
|
+
if (options.hash) {
|
|
166
|
+
args.push("--hash");
|
|
167
|
+
}
|
|
168
|
+
if (options.refreshTokens) {
|
|
169
|
+
args.push("--refresh-tokens");
|
|
170
|
+
}
|
|
171
|
+
if (options.backend) {
|
|
172
|
+
args.push("--backend", options.backend);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Config methods
|
|
177
|
+
async configInfo(): Promise<ConfigInfo> {
|
|
178
|
+
const output = await this.execCli(["info"]);
|
|
179
|
+
const lines = output.trim().split("\n");
|
|
180
|
+
const data: Record<string, string> = {};
|
|
181
|
+
for (const line of lines) {
|
|
182
|
+
const match = line.match(/^(\S+)\s*:\s*(.*)$/);
|
|
183
|
+
if (match) {
|
|
184
|
+
data[match[1]] = match[2];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
cfgNum: parseInt(data["Num"] || "0", 10),
|
|
189
|
+
cfgAuthor: data["Author"] || "",
|
|
190
|
+
cfgDate: data["Date"] || "",
|
|
191
|
+
cfgLog: data["Log"],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async configGet(keys: string[]): Promise<Record<string, any>> {
|
|
196
|
+
const output = await this.execCli(["-json", "get", ...keys]);
|
|
197
|
+
return JSON.parse(output);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async configSet(pairs: Record<string, any>, log?: string): Promise<void> {
|
|
201
|
+
const args: string[] = ["set", "-yes", "1"];
|
|
202
|
+
for (const [key, value] of Object.entries(pairs)) {
|
|
203
|
+
args.push(key, String(value));
|
|
204
|
+
}
|
|
205
|
+
if (log) {
|
|
206
|
+
args.push("-log", log);
|
|
207
|
+
}
|
|
208
|
+
await this.execCli(args);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async configAddKey(key: string, subkey: string, value: string): Promise<void> {
|
|
212
|
+
await this.execCli(["addKey", key, subkey, value]);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async configDelKey(key: string, subkey: string): Promise<void> {
|
|
216
|
+
await this.execCli(["delKey", key, subkey]);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async configSave(): Promise<string> {
|
|
220
|
+
return await this.execCli(["save"]);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async configRestore(json: string): Promise<void> {
|
|
224
|
+
await this.execWithStdin([this.paths.cliPath, "restore", "-yes", "1", "-"], json);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async configMerge(json: string): Promise<void> {
|
|
228
|
+
await this.execWithStdin([this.paths.cliPath, "merge", "-yes", "1", "-"], json);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async configRollback(): Promise<void> {
|
|
232
|
+
await this.execCli(["rollback", "-yes", "1"]);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async configUpdateCache(): Promise<void> {
|
|
236
|
+
await this.execCli(["update-cache"]);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async configTestEmail(destination: string): Promise<void> {
|
|
240
|
+
await this.execCli(["test-email", destination]);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Session methods
|
|
244
|
+
async sessionGet(id: string, options?: SessionGetOptions): Promise<Record<string, any>> {
|
|
245
|
+
const args = ["get", id];
|
|
246
|
+
this.pushSessionGetOptions(args, options);
|
|
247
|
+
const output = await this.execSessions(args);
|
|
248
|
+
return JSON.parse(output);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async sessionSearch(filters: SessionFilter): Promise<any[]> {
|
|
252
|
+
const args: string[] = ["search"];
|
|
253
|
+
if (filters.where) {
|
|
254
|
+
for (const [field, value] of Object.entries(filters.where)) {
|
|
255
|
+
args.push("--where", `${field}=${value}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (filters.select && filters.select.length > 0) {
|
|
259
|
+
args.push("--select", filters.select.join(","));
|
|
260
|
+
}
|
|
261
|
+
if (filters.backend) {
|
|
262
|
+
args.push("--backend", filters.backend);
|
|
263
|
+
}
|
|
264
|
+
if (filters.count) {
|
|
265
|
+
args.push("--count");
|
|
266
|
+
}
|
|
267
|
+
if (filters.refreshTokens) {
|
|
268
|
+
args.push("--refresh-tokens");
|
|
269
|
+
}
|
|
270
|
+
if (filters.persistent) {
|
|
271
|
+
args.push("--persistent");
|
|
272
|
+
}
|
|
273
|
+
if (filters.hash) {
|
|
274
|
+
args.push("--hash");
|
|
275
|
+
}
|
|
276
|
+
if (filters.idOnly) {
|
|
277
|
+
args.push("--id-only");
|
|
278
|
+
}
|
|
279
|
+
const output = await this.execSessions(args);
|
|
280
|
+
return JSON.parse(output);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async sessionDelete(ids: string[], options?: SessionDeleteOptions): Promise<void> {
|
|
284
|
+
if (options?.where) {
|
|
285
|
+
// Where-based deletion uses lemonldap-ng-sessions delete
|
|
286
|
+
const args: string[] = ["delete"];
|
|
287
|
+
for (const [field, value] of Object.entries(options.where)) {
|
|
288
|
+
args.push("--where", `${field}=${value}`);
|
|
289
|
+
}
|
|
290
|
+
this.pushSessionGetOptions(args, options);
|
|
291
|
+
await this.execSessions(args);
|
|
292
|
+
} else {
|
|
293
|
+
// ID-based deletion uses llngDeleteSession
|
|
294
|
+
const deleteScriptPath = this.paths.cliPath.replace("lemonldap-ng-cli", "llngDeleteSession");
|
|
295
|
+
for (const id of ids) {
|
|
296
|
+
const args = [deleteScriptPath, id];
|
|
297
|
+
this.pushSessionGetOptions(args, options);
|
|
298
|
+
await this.exec(args);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async sessionSetKey(
|
|
304
|
+
id: string,
|
|
305
|
+
pairs: Record<string, any>,
|
|
306
|
+
options?: SessionGetOptions,
|
|
307
|
+
): Promise<void> {
|
|
308
|
+
const args: string[] = ["setKey", id];
|
|
309
|
+
for (const [key, value] of Object.entries(pairs)) {
|
|
310
|
+
args.push(key, String(value));
|
|
311
|
+
}
|
|
312
|
+
this.pushSessionGetOptions(args, options);
|
|
313
|
+
await this.execSessions(args);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async sessionDelKey(id: string, keys: string[], options?: SessionGetOptions): Promise<void> {
|
|
317
|
+
const args: string[] = ["delKey", id, ...keys];
|
|
318
|
+
this.pushSessionGetOptions(args, options);
|
|
319
|
+
await this.execSessions(args);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async sessionBackup(
|
|
323
|
+
backend?: string,
|
|
324
|
+
refreshTokens?: boolean,
|
|
325
|
+
persistent?: boolean,
|
|
326
|
+
): Promise<string> {
|
|
327
|
+
const args = ["search"];
|
|
328
|
+
if (backend) {
|
|
329
|
+
args.push("--backend", backend);
|
|
330
|
+
}
|
|
331
|
+
if (refreshTokens) {
|
|
332
|
+
args.push("--refresh-tokens");
|
|
333
|
+
}
|
|
334
|
+
if (persistent) {
|
|
335
|
+
args.push("--persistent");
|
|
336
|
+
}
|
|
337
|
+
const output = await this.execSessions(args);
|
|
338
|
+
return output;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// 2FA methods
|
|
342
|
+
async secondFactorsGet(_user: string): Promise<any[]> {
|
|
343
|
+
throw new Error("secondFactorsGet is not supported via CLI. Use API mode.");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async secondFactorsDelete(_user: string, _ids: string[]): Promise<void> {
|
|
347
|
+
throw new Error("secondFactorsDelete is not supported via CLI. Use API mode.");
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async secondFactorsDelType(_user: string, _type: string): Promise<void> {
|
|
351
|
+
throw new Error("secondFactorsDelType is not supported via CLI. Use API mode.");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Consents methods
|
|
355
|
+
async consentsGet(_user: string): Promise<any[]> {
|
|
356
|
+
throw new Error("consentsGet is not supported via CLI. Use API mode.");
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async consentsDelete(_user: string, _ids: string[]): Promise<void> {
|
|
360
|
+
throw new Error("consentsDelete is not supported via CLI. Use API mode.");
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async execScript(scriptName: string, args: string[]): Promise<string> {
|
|
364
|
+
const prefix = this.config.binPrefix || "/usr/share/lemonldap-ng/bin";
|
|
365
|
+
return this.exec([`${prefix}/${scriptName}`, ...args]);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { LlngMultiConfig, LlngInstanceConfig, LlngConfig, OidcConfig } from "../config.js";
|
|
2
|
+
import { ILlngTransport } from "./interface.js";
|
|
3
|
+
import { ApiTransport } from "./api.js";
|
|
4
|
+
import { SshTransport } from "./ssh.js";
|
|
5
|
+
import { K8sTransport } from "./k8s.js";
|
|
6
|
+
|
|
7
|
+
export type TransportRole = "portal" | "manager";
|
|
8
|
+
|
|
9
|
+
interface TransportEntry {
|
|
10
|
+
portal: ILlngTransport;
|
|
11
|
+
manager?: ILlngTransport;
|
|
12
|
+
managerResolved?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class TransportRegistry {
|
|
16
|
+
private transports = new Map<string, TransportEntry>();
|
|
17
|
+
private configs: Record<string, LlngInstanceConfig>;
|
|
18
|
+
private defaultInstance: string;
|
|
19
|
+
|
|
20
|
+
constructor(multiConfig: LlngMultiConfig) {
|
|
21
|
+
this.configs = multiConfig.instances;
|
|
22
|
+
this.defaultInstance = multiConfig.default;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private resolveInstance(instance?: string): { name: string; config: LlngInstanceConfig } {
|
|
26
|
+
const name = instance || this.defaultInstance;
|
|
27
|
+
const config = this.configs[name];
|
|
28
|
+
if (!config) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Unknown instance '${name}'. Available instances: ${Object.keys(this.configs).join(", ")}`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return { name, config };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private buildTransport(config: LlngConfig): ILlngTransport {
|
|
37
|
+
if (config.mode === "api") {
|
|
38
|
+
if (!config.api) {
|
|
39
|
+
throw new Error(`API mode requires 'api' configuration`);
|
|
40
|
+
}
|
|
41
|
+
return new ApiTransport(config.api);
|
|
42
|
+
} else if (config.mode === "k8s") {
|
|
43
|
+
if (!config.k8s) {
|
|
44
|
+
throw new Error(`K8s mode requires 'k8s' configuration`);
|
|
45
|
+
}
|
|
46
|
+
return new K8sTransport(config.k8s);
|
|
47
|
+
} else {
|
|
48
|
+
return new SshTransport(config.ssh ?? {});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private buildManagerConfig(config: LlngInstanceConfig): LlngConfig | undefined {
|
|
53
|
+
if (!config.manager) return undefined;
|
|
54
|
+
const m = config.manager;
|
|
55
|
+
const merged: LlngConfig = {
|
|
56
|
+
mode: m.mode ?? config.mode,
|
|
57
|
+
};
|
|
58
|
+
// Deep merge ssh: parent ssh + manager ssh overrides
|
|
59
|
+
if (config.mode === "ssh" || merged.mode === "ssh") {
|
|
60
|
+
merged.ssh = { ...config.ssh, ...m.ssh };
|
|
61
|
+
}
|
|
62
|
+
if (m.api) merged.api = m.api;
|
|
63
|
+
else if (config.api && merged.mode === "api") merged.api = config.api;
|
|
64
|
+
if (m.k8s) merged.k8s = m.k8s;
|
|
65
|
+
else if (config.k8s && merged.mode === "k8s") merged.k8s = config.k8s;
|
|
66
|
+
return merged;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
getTransport(instance?: string, role?: TransportRole): ILlngTransport {
|
|
70
|
+
const { name, config } = this.resolveInstance(instance);
|
|
71
|
+
|
|
72
|
+
let entry = this.transports.get(name);
|
|
73
|
+
if (!entry) {
|
|
74
|
+
const portal = this.buildTransport(config);
|
|
75
|
+
entry = { portal };
|
|
76
|
+
this.transports.set(name, entry);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (role === "manager") {
|
|
80
|
+
if (!entry.managerResolved) {
|
|
81
|
+
const managerConfig = this.buildManagerConfig(config);
|
|
82
|
+
entry.manager = managerConfig ? this.buildTransport(managerConfig) : undefined;
|
|
83
|
+
entry.managerResolved = true;
|
|
84
|
+
}
|
|
85
|
+
if (entry.manager) {
|
|
86
|
+
return entry.manager;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return entry.portal;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getOidcConfig(instance?: string): OidcConfig | undefined {
|
|
93
|
+
const { config } = this.resolveInstance(instance);
|
|
94
|
+
return config.oidc;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
listInstances(): { name: string; mode: string; isDefault: boolean; hasManager: boolean }[] {
|
|
98
|
+
return Object.entries(this.configs).map(([name, config]) => ({
|
|
99
|
+
name,
|
|
100
|
+
mode: config.mode,
|
|
101
|
+
isDefault: name === this.defaultInstance,
|
|
102
|
+
hasManager: !!config.manager,
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
}
|