@compforge/harness-toolbox 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.
Files changed (92) hide show
  1. package/README.md +47 -0
  2. package/dist/chunk-0rs1aacd.js +127 -0
  3. package/dist/chunk-1at7zs0z.js +104 -0
  4. package/dist/chunk-1vfwvgkj.js +104 -0
  5. package/dist/chunk-205a4vma.js +85 -0
  6. package/dist/chunk-3dt25p98.js +59 -0
  7. package/dist/chunk-6fnhzhdb.js +103 -0
  8. package/dist/chunk-8nm9f0hh.js +92 -0
  9. package/dist/chunk-ajhdd4hp.js +267 -0
  10. package/dist/chunk-c6z919qb.js +98 -0
  11. package/dist/chunk-djk4kmxb.js +0 -0
  12. package/dist/chunk-hqx7j9xe.js +14 -0
  13. package/dist/chunk-hv2kx47y.js +92 -0
  14. package/dist/chunk-j46x7j7c.js +73 -0
  15. package/dist/chunk-jr9y0ghr.js +157 -0
  16. package/dist/chunk-mkhyty75.js +166 -0
  17. package/dist/chunk-mp88s7r7.js +12 -0
  18. package/dist/chunk-pfar9cm3.js +0 -0
  19. package/dist/chunk-qq77x9ek.js +34 -0
  20. package/dist/chunk-s06hs7a8.js +79 -0
  21. package/dist/chunk-s1sjyem7.js +41 -0
  22. package/dist/chunk-v3hxvnrc.js +47 -0
  23. package/dist/chunk-w3a4p6jm.js +124 -0
  24. package/dist/chunk-xy4g56g4.js +50 -0
  25. package/dist/chunk-ztenxgjv.js +27 -0
  26. package/dist/client-manager.d.ts +14 -0
  27. package/dist/client-manager.js +6 -0
  28. package/dist/client.d.ts +5 -0
  29. package/dist/client.js +0 -0
  30. package/dist/concurrency.d.ts +7 -0
  31. package/dist/concurrency.js +6 -0
  32. package/dist/datasource.d.ts +26 -0
  33. package/dist/datasource.js +10 -0
  34. package/dist/kubernetes/client-node-pod-log.d.ts +31 -0
  35. package/dist/kubernetes/client-node-pod-log.js +425 -0
  36. package/dist/kubernetes/client.d.ts +17 -0
  37. package/dist/kubernetes/client.js +92 -0
  38. package/dist/kubernetes/endpoint.d.ts +4 -0
  39. package/dist/kubernetes/endpoint.js +0 -0
  40. package/dist/kubernetes/executor.d.ts +45 -0
  41. package/dist/kubernetes/executor.js +13 -0
  42. package/dist/kubernetes/log-capture-file.d.ts +12 -0
  43. package/dist/kubernetes/log-capture-file.js +6 -0
  44. package/dist/kubernetes/log-capture-plan.d.ts +29 -0
  45. package/dist/kubernetes/log-capture-plan.js +9 -0
  46. package/dist/kubernetes/log-timestamp.d.ts +2 -0
  47. package/dist/kubernetes/log-timestamp.js +6 -0
  48. package/dist/kubernetes/pod-log-client.d.ts +25 -0
  49. package/dist/kubernetes/pod-log-client.js +12 -0
  50. package/dist/kubernetes/pod-log-datasource.d.ts +17 -0
  51. package/dist/kubernetes/pod-log-datasource.js +31 -0
  52. package/dist/kubernetes/pod-log.d.ts +52 -0
  53. package/dist/kubernetes/pod-log.js +121 -0
  54. package/dist/kubernetes/pod.d.ts +61 -0
  55. package/dist/kubernetes/pod.js +8 -0
  56. package/dist/kubernetes/port-forward.d.ts +42 -0
  57. package/dist/kubernetes/port-forward.js +10 -0
  58. package/dist/kubernetes/service-port-forward.d.ts +29 -0
  59. package/dist/kubernetes/service-port-forward.js +11 -0
  60. package/dist/kubernetes/service.d.ts +31 -0
  61. package/dist/kubernetes/service.js +17 -0
  62. package/dist/mysql/client.d.ts +36 -0
  63. package/dist/mysql/client.js +13 -0
  64. package/dist/mysql/index.d.ts +3 -0
  65. package/dist/mysql/index.js +18 -0
  66. package/dist/mysql/pod.d.ts +7 -0
  67. package/dist/mysql/pod.js +8 -0
  68. package/dist/mysql/target.d.ts +12 -0
  69. package/dist/mysql/target.js +6 -0
  70. package/dist/mysql/types.d.ts +16 -0
  71. package/dist/mysql/types.js +1 -0
  72. package/dist/opensearch/client.d.ts +62 -0
  73. package/dist/opensearch/client.js +12 -0
  74. package/dist/opensearch/endpoint.d.ts +16 -0
  75. package/dist/opensearch/endpoint.js +6 -0
  76. package/dist/opensearch/index.d.ts +3 -0
  77. package/dist/opensearch/index.js +17 -0
  78. package/dist/opensearch/types.d.ts +8 -0
  79. package/dist/opensearch/types.js +1 -0
  80. package/dist/process/index.d.ts +13 -0
  81. package/dist/process/index.js +8 -0
  82. package/dist/redis/client.d.ts +90 -0
  83. package/dist/redis/client.js +14 -0
  84. package/dist/redis/index.d.ts +3 -0
  85. package/dist/redis/index.js +24 -0
  86. package/dist/redis/key-stats.d.ts +27 -0
  87. package/dist/redis/key-stats.js +8 -0
  88. package/dist/redis/topology.d.ts +23 -0
  89. package/dist/redis/topology.js +6 -0
  90. package/dist/transport/index.d.ts +40 -0
  91. package/dist/transport/index.js +12 -0
  92. package/package.json +96 -0
@@ -0,0 +1,92 @@
1
+ import {
2
+ ConcurrencyPool
3
+ } from "./chunk-v3hxvnrc.js";
4
+
5
+ // src/kubernetes/log-capture-plan.ts
6
+ function budgetUnavailable(request) {
7
+ return {
8
+ ok: false,
9
+ exitCode: null,
10
+ stdout: "",
11
+ stderr: "日志采集总字节预算已耗尽",
12
+ durationMs: 0,
13
+ timedOut: false,
14
+ command: ["kubernetes-api", "logs", request.pod, ...request.container ? ["-c", request.container] : []],
15
+ captureStatus: "unavailable",
16
+ reason: "total_byte_budget",
17
+ bytesRead: 0,
18
+ attempts: 0
19
+ };
20
+ }
21
+
22
+ class PodLogByteBudget {
23
+ #available;
24
+ #reserved = 0;
25
+ #waiters = new Set;
26
+ constructor(maxBytes) {
27
+ this.#available = maxBytes;
28
+ }
29
+ async reserve(maxBytes) {
30
+ while (this.#available <= 0 && this.#reserved > 0) {
31
+ await new Promise((resolve) => this.#waiters.add(resolve));
32
+ }
33
+ const reserved = Math.max(0, Math.min(maxBytes, this.#available));
34
+ this.#available -= reserved;
35
+ this.#reserved += reserved;
36
+ return reserved;
37
+ }
38
+ settle(reserved, bytesRead) {
39
+ this.#reserved -= reserved;
40
+ this.#available += reserved - bytesRead;
41
+ for (const resolve of this.#waiters)
42
+ resolve();
43
+ this.#waiters.clear();
44
+ }
45
+ }
46
+ async function runPodLogCapturePlan(access, plan, policy, pool = new ConcurrencyPool(policy.concurrency), signal, budget = new PodLogByteBudget(policy.maxTotalBytes)) {
47
+ if (!Number.isInteger(policy.concurrency) || policy.concurrency < 1) {
48
+ throw new Error("Pod Log capture concurrency 必须是正整数");
49
+ }
50
+ if (policy.maxBytesPerCapture < 1 || policy.maxTotalBytes < 1) {
51
+ throw new Error("Pod Log capture 字节预算必须为正数");
52
+ }
53
+ const results = new Array(plan.length);
54
+ let cursor = 0;
55
+ const worker = async () => {
56
+ while (cursor < plan.length) {
57
+ const index = cursor++;
58
+ const item = plan[index];
59
+ await pool.run(async () => {
60
+ item.onStart?.();
61
+ const reservedBytes = await budget.reserve(Math.min(policy.maxBytesPerCapture, item.request.limitBytes ?? policy.maxBytesPerCapture));
62
+ if (signal?.aborted) {
63
+ budget.settle(reservedBytes, 0);
64
+ signal.throwIfAborted();
65
+ }
66
+ if (reservedBytes <= 0) {
67
+ results[index] = {
68
+ target: item.target,
69
+ request: item.request,
70
+ capture: budgetUnavailable(item.request)
71
+ };
72
+ return;
73
+ }
74
+ const request = { ...item.request, limitBytes: reservedBytes };
75
+ let capture;
76
+ try {
77
+ capture = await access.collectPodLogs(request);
78
+ results[index] = { target: item.target, request, capture };
79
+ } finally {
80
+ budget.settle(reservedBytes, capture?.bytesRead ?? reservedBytes);
81
+ }
82
+ }, signal);
83
+ }
84
+ };
85
+ const settled = await Promise.allSettled(Array.from({ length: Math.min(policy.concurrency, plan.length) }, () => worker()));
86
+ const failed = settled.find((result) => result.status === "rejected");
87
+ if (failed?.status === "rejected" && !signal?.aborted)
88
+ throw failed.reason;
89
+ return results.filter((result) => result !== undefined);
90
+ }
91
+
92
+ export { PodLogByteBudget, runPodLogCapturePlan };
@@ -0,0 +1,267 @@
1
+ // src/redis/client.ts
2
+ import { createClient } from "@redis/client";
3
+ function stringValue(value) {
4
+ if (Buffer.isBuffer(value))
5
+ return value.toString("utf8");
6
+ return String(value ?? "");
7
+ }
8
+ function scalar(value) {
9
+ if (/^-?(?:\d+|\d+\.\d+)$/.test(value))
10
+ return Number(value);
11
+ return value;
12
+ }
13
+ function parseRedisInfo(raw) {
14
+ const result = {};
15
+ for (const line of raw.split(/\r?\n/)) {
16
+ if (!line || line.startsWith("#"))
17
+ continue;
18
+ const separator = line.indexOf(":");
19
+ if (separator < 1)
20
+ continue;
21
+ const name = line.slice(0, separator);
22
+ const value = line.slice(separator + 1);
23
+ if (/^db\d+$/.test(name)) {
24
+ result[name] = Object.fromEntries(value.split(",").map((item) => {
25
+ const index = item.indexOf("=");
26
+ return [item.slice(0, index), scalar(item.slice(index + 1))];
27
+ }));
28
+ } else {
29
+ result[name] = scalar(value);
30
+ }
31
+ }
32
+ return result;
33
+ }
34
+
35
+ class RedisConnection {
36
+ endpoint;
37
+ client;
38
+ timeoutMs;
39
+ lastError;
40
+ constructor(endpoint, client, timeoutMs, lastError) {
41
+ this.endpoint = endpoint;
42
+ this.client = client;
43
+ this.timeoutMs = timeoutMs;
44
+ this.lastError = lastError;
45
+ }
46
+ static async connect(endpoint, mapped, config, clientFactory = createClient) {
47
+ const baseSocket = {
48
+ host: mapped.host,
49
+ port: mapped.port,
50
+ connectTimeout: config.timeoutMs,
51
+ reconnectStrategy: (retries) => retries >= 2 ? false : 100 * 2 ** retries
52
+ };
53
+ const socket = config.useSsl ? {
54
+ ...baseSocket,
55
+ tls: true,
56
+ servername: mapped.servername ?? endpoint.host
57
+ } : {
58
+ ...baseSocket,
59
+ tls: false
60
+ };
61
+ const client = clientFactory({
62
+ socket,
63
+ database: config.database,
64
+ username: config.username,
65
+ password: config.password,
66
+ commandOptions: { timeout: config.timeoutMs }
67
+ });
68
+ let lastError;
69
+ client.on("error", (err) => {
70
+ lastError = err instanceof Error ? err : new Error(String(err));
71
+ });
72
+ try {
73
+ await client.connect();
74
+ } catch (error) {
75
+ if (client.isOpen)
76
+ client.destroy();
77
+ throw error;
78
+ }
79
+ return new RedisConnection(endpoint, client, config.timeoutMs, () => lastError);
80
+ }
81
+ get isReady() {
82
+ return this.client.isReady;
83
+ }
84
+ async#execute(operationName, operation) {
85
+ try {
86
+ return await this.#executeOnce(operationName, operation);
87
+ } catch (err) {
88
+ if (this.client.isOpen && !this.client.isReady)
89
+ return this.#executeOnce(operationName, operation);
90
+ throw err;
91
+ }
92
+ }
93
+ async#executeOnce(operationName, operation) {
94
+ if (!this.client.isOpen) {
95
+ const reason = this.lastError()?.message;
96
+ throw new Error(`Redis connection is closed${reason ? `: ${reason}` : ""}`);
97
+ }
98
+ let timer;
99
+ const deadline = new Promise((_, reject) => {
100
+ timer = setTimeout(() => {
101
+ reject(new Error(`Redis ${operationName} timed out after ${this.timeoutMs}ms (${this.endpoint.host}:${this.endpoint.port})`));
102
+ this.close();
103
+ }, this.timeoutMs);
104
+ });
105
+ try {
106
+ return await Promise.race([Promise.resolve().then(operation), deadline]);
107
+ } finally {
108
+ if (timer)
109
+ clearTimeout(timer);
110
+ }
111
+ }
112
+ ping() {
113
+ return this.#execute("PING", () => this.client.ping());
114
+ }
115
+ async info(section) {
116
+ const raw = await this.#execute(section ? `INFO ${section}` : "INFO", () => section ? this.client.sendCommand(["INFO", section]) : this.client.sendCommand(["INFO"]));
117
+ return parseRedisInfo(stringValue(raw));
118
+ }
119
+ async dbSize() {
120
+ return Number(await this.#execute("DBSIZE", () => this.client.sendCommand(["DBSIZE"])));
121
+ }
122
+ command(args) {
123
+ return this.#execute(args[0]?.toUpperCase() ?? "command", () => this.client.sendCommand(args));
124
+ }
125
+ async pipeline(commands) {
126
+ const commandNames = new Set(commands.map((command) => command[0]?.toUpperCase()).filter(Boolean));
127
+ const operationName = commandNames.size === 1 ? `${[...commandNames][0]} pipeline` : "pipeline";
128
+ return await this.#execute(operationName, () => {
129
+ const pipeline = this.client.multi();
130
+ for (const command of commands)
131
+ pipeline.addCommand(command);
132
+ return pipeline.execAsPipeline();
133
+ });
134
+ }
135
+ async scan(cursor, count) {
136
+ const reply = await this.command(["SCAN", cursor, "COUNT", String(count)]);
137
+ if (!Array.isArray(reply) || !Array.isArray(reply[1]))
138
+ throw new Error("Redis SCAN 返回格式无效");
139
+ return { cursor: stringValue(reply[0]), keys: reply[1].map(stringValue) };
140
+ }
141
+ close() {
142
+ if (this.client.isOpen)
143
+ this.client.destroy();
144
+ }
145
+ }
146
+
147
+ class RedisAccess {
148
+ transport;
149
+ baseConfig;
150
+ connectionFactory;
151
+ #connections = new Map;
152
+ #closing;
153
+ constructor(transport, baseConfig, connectionFactory = RedisConnection.connect) {
154
+ this.transport = transport;
155
+ this.baseConfig = baseConfig;
156
+ this.connectionFactory = connectionFactory;
157
+ }
158
+ async connection(endpoint, database, credentials = this.baseConfig) {
159
+ if (this.#closing)
160
+ throw new Error("Redis access is closed");
161
+ const key = [
162
+ endpoint.host,
163
+ endpoint.port,
164
+ database,
165
+ credentials.username ?? "",
166
+ credentials.password ?? ""
167
+ ].join("\x00");
168
+ let pending = this.#connections.get(key);
169
+ if (pending) {
170
+ const connection = await pending;
171
+ if (this.#closing)
172
+ throw new Error("Redis access is closed");
173
+ if (connection.isReady)
174
+ return connection;
175
+ if (this.#connections.get(key) === pending) {
176
+ this.#connections.delete(key);
177
+ connection.close();
178
+ } else {
179
+ return this.connection(endpoint, database, credentials);
180
+ }
181
+ }
182
+ pending = this.transport.connect(endpoint).then((mapped) => this.connectionFactory(endpoint, mapped, {
183
+ ...this.baseConfig,
184
+ ...credentials,
185
+ database
186
+ }));
187
+ this.#connections.set(key, pending);
188
+ pending.catch(() => {
189
+ if (this.#connections.get(key) === pending)
190
+ this.#connections.delete(key);
191
+ });
192
+ return await pending;
193
+ }
194
+ close() {
195
+ return this.#closing ??= (async () => {
196
+ const settled = await Promise.allSettled(this.#connections.values());
197
+ this.#connections.clear();
198
+ const errors = [];
199
+ for (const result of settled)
200
+ if (result.status === "fulfilled") {
201
+ try {
202
+ result.value.close();
203
+ } catch (error) {
204
+ errors.push(error);
205
+ }
206
+ }
207
+ if (errors.length)
208
+ throw new AggregateError(errors, "Redis connection cleanup failed");
209
+ })();
210
+ }
211
+ }
212
+
213
+ class RedisClient {
214
+ source;
215
+ #controller = new AbortController;
216
+ signal;
217
+ #initialization;
218
+ #disposal;
219
+ #access;
220
+ #target;
221
+ constructor(source, lifecycle = {}) {
222
+ this.source = source;
223
+ this.signal = lifecycle.signal ? AbortSignal.any([lifecycle.signal, this.#controller.signal]) : this.#controller.signal;
224
+ }
225
+ initialize() {
226
+ this.signal.throwIfAborted();
227
+ return this.#initialization ??= (async () => {
228
+ this.#target = await this.source.resolve();
229
+ this.signal.throwIfAborted();
230
+ const transport = this.source.transports[0];
231
+ if (!transport || transport.kind !== "tcp")
232
+ throw new Error("Redis requires a TCP transport");
233
+ this.#access = new RedisAccess(transport, this.#target);
234
+ })();
235
+ }
236
+ get access() {
237
+ this.signal.throwIfAborted();
238
+ if (!this.#access)
239
+ throw new Error("Redis client is not initialized");
240
+ return this.#access;
241
+ }
242
+ get target() {
243
+ if (!this.#target)
244
+ throw new Error("Redis client is not initialized");
245
+ return this.#target;
246
+ }
247
+ dispose() {
248
+ return this.#disposal ??= (async () => {
249
+ this.#controller.abort(new Error("Redis client disposed"));
250
+ await this.#initialization?.catch(() => {});
251
+ await this.#access?.close();
252
+ })();
253
+ }
254
+ }
255
+ async function openRedis(source, lifecycle = {}) {
256
+ const client = new RedisClient(source, lifecycle);
257
+ try {
258
+ await client.initialize();
259
+ } catch (error) {
260
+ await client.dispose();
261
+ throw error;
262
+ }
263
+ lifecycle.onDispose?.(() => client.dispose());
264
+ return client;
265
+ }
266
+
267
+ export { parseRedisInfo, RedisConnection, RedisAccess, RedisClient, openRedis };
@@ -0,0 +1,98 @@
1
+ // src/redis/topology.ts
2
+ function text(value) {
3
+ return Buffer.isBuffer(value) ? value.toString("utf8") : String(value ?? "");
4
+ }
5
+ function endpoint(value, fallbackHost) {
6
+ if (!Array.isArray(value) || value.length < 2)
7
+ throw new Error("Redis 拓扑 endpoint 格式无效");
8
+ const host = text(value[0]) || fallbackHost || "";
9
+ const port = Number(value[1]);
10
+ if (!host || !Number.isInteger(port) || port < 1 || port > 65535) {
11
+ throw new Error(`Redis 拓扑 endpoint 无效:${host || "(empty)"}:${text(value[1])}`);
12
+ }
13
+ return { host, port };
14
+ }
15
+ function sentinelRows(value) {
16
+ if (!Array.isArray(value))
17
+ return [];
18
+ return value.flatMap((row) => {
19
+ if (!Array.isArray(row))
20
+ return [];
21
+ const record = {};
22
+ for (let index = 0;index + 1 < row.length; index += 2) {
23
+ record[text(row[index])] = text(row[index + 1]);
24
+ }
25
+ return [record];
26
+ });
27
+ }
28
+ async function discoverSentinel(access, config) {
29
+ const sentinels = config.sentinelHosts.length ? config.sentinelHosts : config.endpoints;
30
+ let lastError;
31
+ for (const sentinel of sentinels) {
32
+ try {
33
+ const client = await access.connection(sentinel, 0, {
34
+ username: config.sentinelUsername,
35
+ password: config.sentinelPassword
36
+ });
37
+ const master = endpoint(await client.command([
38
+ "SENTINEL",
39
+ "GET-MASTER-ADDR-BY-NAME",
40
+ config.sentinelMasterName
41
+ ]));
42
+ const replicas = sentinelRows(await client.command([
43
+ "SENTINEL",
44
+ "REPLICAS",
45
+ config.sentinelMasterName
46
+ ])).filter((row) => !row.flags?.includes("s_down") && !row.flags?.includes("o_down")).flatMap((row) => {
47
+ const port = Number(row.port);
48
+ return row.ip && Number.isInteger(port) && port > 0 && port <= 65535 ? [{ host: row.ip, port }] : [];
49
+ });
50
+ return { clusterType: "sentinel", masters: [master], replicas, slotRanges: [] };
51
+ } catch (err) {
52
+ lastError = err;
53
+ }
54
+ }
55
+ throw lastError ?? new Error("没有可用的 Redis Sentinel endpoint");
56
+ }
57
+ async function discoverSeed(access, config) {
58
+ let lastError;
59
+ for (const seed of config.endpoints) {
60
+ try {
61
+ const client = await access.connection(seed, 0);
62
+ await client.ping();
63
+ const clusterInfo = await client.info("cluster");
64
+ if (config.clusterType !== "cluster" && Number(clusterInfo.cluster_enabled ?? 0) !== 1) {
65
+ return { clusterType: "single", masters: [seed], replicas: [], slotRanges: [] };
66
+ }
67
+ const reply = await client.command(["CLUSTER", "SLOTS"]);
68
+ if (!Array.isArray(reply))
69
+ throw new Error("CLUSTER SLOTS 返回格式无效");
70
+ const masters = new Map;
71
+ const replicas = new Map;
72
+ const slotRanges = reply.map((row) => {
73
+ if (!Array.isArray(row) || row.length < 3)
74
+ throw new Error("CLUSTER SLOTS row 格式无效");
75
+ const master = endpoint(row[2], seed.host);
76
+ masters.set(`${master.host}:${master.port}`, master);
77
+ const rowReplicas = row.slice(3).map((item) => endpoint(item, seed.host));
78
+ for (const replica of rowReplicas)
79
+ replicas.set(`${replica.host}:${replica.port}`, replica);
80
+ return { start: Number(row[0]), end: Number(row[1]), master, replicas: rowReplicas };
81
+ });
82
+ return {
83
+ clusterType: "cluster",
84
+ masters: [...masters.values()],
85
+ replicas: [...replicas.values()],
86
+ slotRanges
87
+ };
88
+ } catch (err) {
89
+ lastError = err;
90
+ }
91
+ }
92
+ throw lastError ?? new Error("没有可用的 Redis endpoint");
93
+ }
94
+ function discoverRedisTopology(access, config) {
95
+ return config.clusterType === "sentinel" ? discoverSentinel(access, config) : discoverSeed(access, config);
96
+ }
97
+
98
+ export { discoverRedisTopology };
File without changes
@@ -0,0 +1,14 @@
1
+ // src/datasource.ts
2
+ import { createHash } from "node:crypto";
3
+ function dataSourceKey(protocol, configuration) {
4
+ const canonical = (value) => {
5
+ if (Array.isArray(value))
6
+ return value.map(canonical);
7
+ if (value && typeof value === "object")
8
+ return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, canonical(item)]));
9
+ return value;
10
+ };
11
+ return `${protocol}:${createHash("sha256").update(JSON.stringify(canonical(configuration))).digest("hex")}`;
12
+ }
13
+
14
+ export { dataSourceKey };
@@ -0,0 +1,92 @@
1
+ import {
2
+ buildKubectlArgs
3
+ } from "./chunk-0rs1aacd.js";
4
+ import {
5
+ sleep,
6
+ spawnProcess
7
+ } from "./chunk-ztenxgjv.js";
8
+
9
+ // src/kubernetes/port-forward.ts
10
+ async function startPortForward(opts) {
11
+ const target = opts.target ?? (opts.service ? { kind: "service", name: opts.service } : undefined);
12
+ if (!target)
13
+ return { ok: false, reason: "port-forward 缺少 Service/Pod 目标" };
14
+ const argv = buildKubectlArgs(opts, [
15
+ "port-forward",
16
+ `${target.kind === "service" ? "svc" : "pod"}/${target.name}`,
17
+ `0:${opts.remotePort}`,
18
+ "--address",
19
+ "127.0.0.1"
20
+ ]);
21
+ let proc;
22
+ try {
23
+ proc = spawnProcess(argv);
24
+ } catch (err) {
25
+ return { ok: false, reason: err instanceof Error ? err.message : String(err) };
26
+ }
27
+ const timeoutMs = opts.timeoutMs ?? 15000;
28
+ const stderrPromise = new Response(proc.stderr).text();
29
+ const reader = proc.stdout.getReader();
30
+ const decoder = new TextDecoder;
31
+ const deadline = Date.now() + timeoutMs;
32
+ let out = "";
33
+ for (;; ) {
34
+ const remain = deadline - Date.now();
35
+ if (remain <= 0)
36
+ break;
37
+ const chunk = await Promise.race([
38
+ reader.read(),
39
+ sleep(remain).then(() => "timeout"),
40
+ proc.exited.then(() => "exited").catch((error) => ({ error }))
41
+ ]);
42
+ if (chunk === "timeout")
43
+ break;
44
+ if (typeof chunk === "object" && "error" in chunk) {
45
+ proc.kill();
46
+ return { ok: false, reason: chunk.error instanceof Error ? chunk.error.message : String(chunk.error) };
47
+ }
48
+ if (chunk === "exited" || chunk.done)
49
+ break;
50
+ out += decoder.decode(chunk.value);
51
+ const m = out.match(/Forwarding from 127\.0\.0\.1:(\d+)/);
52
+ if (m) {
53
+ return {
54
+ ok: true,
55
+ value: { target, localPort: Number(m[1]), command: argv, stop: () => proc.kill() }
56
+ };
57
+ }
58
+ }
59
+ proc.kill();
60
+ const stderr = await stderrPromise.catch(() => "");
61
+ const reason = stderr.trim().split(`
62
+ `)[0] || `port-forward 未就绪(${timeoutMs}ms 超时)`;
63
+ return { ok: false, reason };
64
+ }
65
+
66
+ class PortForwardScope {
67
+ starter;
68
+ #forwards = [];
69
+ #stopOnExit = () => this.stop();
70
+ constructor(starter = startPortForward) {
71
+ this.starter = starter;
72
+ }
73
+ get active() {
74
+ return this.#forwards;
75
+ }
76
+ async start(opts) {
77
+ const result = await this.starter(opts);
78
+ if (result.value) {
79
+ if (this.#forwards.length === 0)
80
+ process.once("exit", this.#stopOnExit);
81
+ this.#forwards.push(result.value);
82
+ }
83
+ return result;
84
+ }
85
+ stop() {
86
+ process.off("exit", this.#stopOnExit);
87
+ for (const forward of this.#forwards.splice(0).reverse())
88
+ forward.stop();
89
+ }
90
+ }
91
+
92
+ export { startPortForward, PortForwardScope };
@@ -0,0 +1,73 @@
1
+ import {
2
+ parsePods
3
+ } from "./chunk-6fnhzhdb.js";
4
+
5
+ // src/kubernetes/service.ts
6
+ function parseList(raw) {
7
+ const value = JSON.parse(raw);
8
+ return value.items ?? [];
9
+ }
10
+ function parseServices(raw, defaultNamespace) {
11
+ return parseList(raw).flatMap((item) => {
12
+ const name = item.metadata?.name;
13
+ if (!name)
14
+ return [];
15
+ const ports = (item.spec?.ports ?? []).flatMap((port) => Number.isInteger(port.port) ? [{ name: port.name, port: port.port, targetPort: port.targetPort }] : []);
16
+ return [{
17
+ kind: "service",
18
+ namespace: item.metadata?.namespace ?? defaultNamespace,
19
+ name,
20
+ clusterIP: item.spec?.clusterIP,
21
+ selector: item.spec?.selector ?? {},
22
+ ports
23
+ }];
24
+ });
25
+ }
26
+ function serviceIdentity(host, defaultNamespace) {
27
+ const labels = host.replace(/\.$/, "").split(".");
28
+ if (labels.length === 1)
29
+ return { name: labels[0], namespace: defaultNamespace };
30
+ if (labels.length === 2)
31
+ return { name: labels[0], namespace: labels[1] };
32
+ if (labels.length >= 3 && labels[2] === "svc") {
33
+ return { name: labels[0], namespace: labels[1] };
34
+ }
35
+ return;
36
+ }
37
+ function findService(services, endpoint, defaultNamespace) {
38
+ const identity = serviceIdentity(endpoint.host, defaultNamespace);
39
+ return services.find((service) => (identity ? service.name === identity.name && service.namespace === identity.namespace : service.clusterIP === endpoint.host) && service.ports.some((port) => port.port === endpoint.port));
40
+ }
41
+ function findServicesForPod(services, pod) {
42
+ return services.filter((service) => {
43
+ const selector = Object.entries(service.selector);
44
+ return service.namespace === pod.namespace && selector.length > 0 && selector.every(([name, value]) => pod.labels[name] === value);
45
+ });
46
+ }
47
+ function findPodsForService(services, pods, serviceName, namespace) {
48
+ const service = services.find((item) => item.name === serviceName && item.namespace === namespace);
49
+ if (!service)
50
+ return [];
51
+ const selector = Object.entries(service.selector);
52
+ if (!selector.length)
53
+ return [];
54
+ return pods.filter((pod) => pod.namespace === namespace && pod.phase === "Running" && selector.every(([name, value]) => pod.labels[name] === value)).sort((left, right) => left.name.localeCompare(right.name));
55
+ }
56
+ async function listServiceNetwork(executor, namespace) {
57
+ const [serviceResult, podResult] = await Promise.all([
58
+ executor.run(["get", "services", "-o", "json"], { timeoutMs: 20000 }),
59
+ executor.run(["get", "pods", "-o", "json"], { timeoutMs: 20000 })
60
+ ]);
61
+ if (!serviceResult.ok) {
62
+ throw new Error(`读取 Service 失败:${serviceResult.stderr.trim() || `exit=${serviceResult.exitCode}`}`);
63
+ }
64
+ if (!podResult.ok) {
65
+ throw new Error(`读取 Pod 网络信息失败:${podResult.stderr.trim() || `exit=${podResult.exitCode}`}`);
66
+ }
67
+ return {
68
+ services: parseServices(serviceResult.stdout, namespace),
69
+ pods: parsePods(podResult.stdout, namespace)
70
+ };
71
+ }
72
+
73
+ export { parseServices, serviceIdentity, findService, findServicesForPod, findPodsForService, listServiceNetwork };