@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.
- package/README.md +47 -0
- package/dist/chunk-0rs1aacd.js +127 -0
- package/dist/chunk-1at7zs0z.js +104 -0
- package/dist/chunk-1vfwvgkj.js +104 -0
- package/dist/chunk-205a4vma.js +85 -0
- package/dist/chunk-3dt25p98.js +59 -0
- package/dist/chunk-6fnhzhdb.js +103 -0
- package/dist/chunk-8nm9f0hh.js +92 -0
- package/dist/chunk-ajhdd4hp.js +267 -0
- package/dist/chunk-c6z919qb.js +98 -0
- package/dist/chunk-djk4kmxb.js +0 -0
- package/dist/chunk-hqx7j9xe.js +14 -0
- package/dist/chunk-hv2kx47y.js +92 -0
- package/dist/chunk-j46x7j7c.js +73 -0
- package/dist/chunk-jr9y0ghr.js +157 -0
- package/dist/chunk-mkhyty75.js +166 -0
- package/dist/chunk-mp88s7r7.js +12 -0
- package/dist/chunk-pfar9cm3.js +0 -0
- package/dist/chunk-qq77x9ek.js +34 -0
- package/dist/chunk-s06hs7a8.js +79 -0
- package/dist/chunk-s1sjyem7.js +41 -0
- package/dist/chunk-v3hxvnrc.js +47 -0
- package/dist/chunk-w3a4p6jm.js +124 -0
- package/dist/chunk-xy4g56g4.js +50 -0
- package/dist/chunk-ztenxgjv.js +27 -0
- package/dist/client-manager.d.ts +14 -0
- package/dist/client-manager.js +6 -0
- package/dist/client.d.ts +5 -0
- package/dist/client.js +0 -0
- package/dist/concurrency.d.ts +7 -0
- package/dist/concurrency.js +6 -0
- package/dist/datasource.d.ts +26 -0
- package/dist/datasource.js +10 -0
- package/dist/kubernetes/client-node-pod-log.d.ts +31 -0
- package/dist/kubernetes/client-node-pod-log.js +425 -0
- package/dist/kubernetes/client.d.ts +17 -0
- package/dist/kubernetes/client.js +92 -0
- package/dist/kubernetes/endpoint.d.ts +4 -0
- package/dist/kubernetes/endpoint.js +0 -0
- package/dist/kubernetes/executor.d.ts +45 -0
- package/dist/kubernetes/executor.js +13 -0
- package/dist/kubernetes/log-capture-file.d.ts +12 -0
- package/dist/kubernetes/log-capture-file.js +6 -0
- package/dist/kubernetes/log-capture-plan.d.ts +29 -0
- package/dist/kubernetes/log-capture-plan.js +9 -0
- package/dist/kubernetes/log-timestamp.d.ts +2 -0
- package/dist/kubernetes/log-timestamp.js +6 -0
- package/dist/kubernetes/pod-log-client.d.ts +25 -0
- package/dist/kubernetes/pod-log-client.js +12 -0
- package/dist/kubernetes/pod-log-datasource.d.ts +17 -0
- package/dist/kubernetes/pod-log-datasource.js +31 -0
- package/dist/kubernetes/pod-log.d.ts +52 -0
- package/dist/kubernetes/pod-log.js +121 -0
- package/dist/kubernetes/pod.d.ts +61 -0
- package/dist/kubernetes/pod.js +8 -0
- package/dist/kubernetes/port-forward.d.ts +42 -0
- package/dist/kubernetes/port-forward.js +10 -0
- package/dist/kubernetes/service-port-forward.d.ts +29 -0
- package/dist/kubernetes/service-port-forward.js +11 -0
- package/dist/kubernetes/service.d.ts +31 -0
- package/dist/kubernetes/service.js +17 -0
- package/dist/mysql/client.d.ts +36 -0
- package/dist/mysql/client.js +13 -0
- package/dist/mysql/index.d.ts +3 -0
- package/dist/mysql/index.js +18 -0
- package/dist/mysql/pod.d.ts +7 -0
- package/dist/mysql/pod.js +8 -0
- package/dist/mysql/target.d.ts +12 -0
- package/dist/mysql/target.js +6 -0
- package/dist/mysql/types.d.ts +16 -0
- package/dist/mysql/types.js +1 -0
- package/dist/opensearch/client.d.ts +62 -0
- package/dist/opensearch/client.js +12 -0
- package/dist/opensearch/endpoint.d.ts +16 -0
- package/dist/opensearch/endpoint.js +6 -0
- package/dist/opensearch/index.d.ts +3 -0
- package/dist/opensearch/index.js +17 -0
- package/dist/opensearch/types.d.ts +8 -0
- package/dist/opensearch/types.js +1 -0
- package/dist/process/index.d.ts +13 -0
- package/dist/process/index.js +8 -0
- package/dist/redis/client.d.ts +90 -0
- package/dist/redis/client.js +14 -0
- package/dist/redis/index.d.ts +3 -0
- package/dist/redis/index.js +24 -0
- package/dist/redis/key-stats.d.ts +27 -0
- package/dist/redis/key-stats.js +8 -0
- package/dist/redis/topology.d.ts +23 -0
- package/dist/redis/topology.js +6 -0
- package/dist/transport/index.d.ts +40 -0
- package/dist/transport/index.js +12 -0
- package/package.json +96 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// src/transport/index.ts
|
|
2
|
+
class DirectTransport {
|
|
3
|
+
kind = "tcp";
|
|
4
|
+
name = "direct";
|
|
5
|
+
async connect(endpoint) {
|
|
6
|
+
return endpoint;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
class PortForwardTransport {
|
|
11
|
+
forward;
|
|
12
|
+
kind = "tcp";
|
|
13
|
+
name = "port-forward";
|
|
14
|
+
constructor(forward) {
|
|
15
|
+
this.forward = forward;
|
|
16
|
+
}
|
|
17
|
+
connect(endpoint) {
|
|
18
|
+
return this.forward(endpoint);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class PodPythonTransport {
|
|
23
|
+
exec;
|
|
24
|
+
python;
|
|
25
|
+
kind = "python";
|
|
26
|
+
name = "pod-python";
|
|
27
|
+
constructor(exec, python = "python") {
|
|
28
|
+
this.exec = exec;
|
|
29
|
+
this.python = python;
|
|
30
|
+
}
|
|
31
|
+
run(script, input, timeoutMs) {
|
|
32
|
+
return this.exec([this.python, "-c", script], { stdin: JSON.stringify(input), timeoutMs });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function isConnectionNetworkError(error) {
|
|
36
|
+
if (!error || typeof error !== "object" || !("code" in error))
|
|
37
|
+
return false;
|
|
38
|
+
return [
|
|
39
|
+
"ECONNREFUSED",
|
|
40
|
+
"ETIMEDOUT",
|
|
41
|
+
"ECONNRESET",
|
|
42
|
+
"EHOSTUNREACH",
|
|
43
|
+
"ENETUNREACH",
|
|
44
|
+
"EADDRNOTAVAIL",
|
|
45
|
+
"ENOTFOUND",
|
|
46
|
+
"EAI_AGAIN"
|
|
47
|
+
].includes(String(error.code));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { DirectTransport, PortForwardTransport, PodPythonTransport, isConnectionNetworkError };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/process/index.ts
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { Readable } from "node:stream";
|
|
4
|
+
function spawnProcess(argv, opts) {
|
|
5
|
+
if (!argv.length)
|
|
6
|
+
throw new Error("command argv cannot be empty");
|
|
7
|
+
const child = spawn(argv[0], argv.slice(1), {
|
|
8
|
+
stdio: "pipe",
|
|
9
|
+
env: opts?.env
|
|
10
|
+
});
|
|
11
|
+
child.stdin.end(opts?.stdin);
|
|
12
|
+
const exited = new Promise((resolve, reject) => {
|
|
13
|
+
child.once("error", reject);
|
|
14
|
+
child.once("close", (code) => resolve(code ?? 1));
|
|
15
|
+
});
|
|
16
|
+
return {
|
|
17
|
+
stdout: Readable.toWeb(child.stdout),
|
|
18
|
+
stderr: Readable.toWeb(child.stderr),
|
|
19
|
+
exited,
|
|
20
|
+
kill: () => child.kill()
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function sleep(ms) {
|
|
24
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { spawnProcess, sleep };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Client } from "./client";
|
|
2
|
+
import type { DataSource } from "./datasource";
|
|
3
|
+
/** Consumers borrow clients; only the root owner receives the disposal capability. */
|
|
4
|
+
export interface ClientProvider {
|
|
5
|
+
get<C extends Client>(source: DataSource<C>): Promise<C>;
|
|
6
|
+
}
|
|
7
|
+
/** @spec One root execution shares successful initialization per identity and closes clients at finalize. */
|
|
8
|
+
export declare class ClientManager implements ClientProvider {
|
|
9
|
+
#private;
|
|
10
|
+
readonly signal: AbortSignal;
|
|
11
|
+
constructor(signal?: AbortSignal);
|
|
12
|
+
get<C extends Client>(source: DataSource<C>): Promise<C>;
|
|
13
|
+
dispose(): Promise<void>;
|
|
14
|
+
}
|
package/dist/client.d.ts
ADDED
package/dist/client.js
ADDED
|
File without changes
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** FIFO slots shared by callers; queued cancellation never starts external work. */
|
|
2
|
+
export declare class ConcurrencyPool {
|
|
3
|
+
#private;
|
|
4
|
+
readonly concurrency: number;
|
|
5
|
+
constructor(concurrency: number);
|
|
6
|
+
run<T>(work: () => Promise<T>, signal?: AbortSignal): Promise<T>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type { Client } from "./client";
|
|
2
|
+
export { ClientManager, type ClientProvider } from "./client-manager";
|
|
3
|
+
import type { Client } from "./client";
|
|
4
|
+
import type { Transport } from "./transport";
|
|
5
|
+
/** Identity must include protocol, target, configuration and credentials; never an object address. */
|
|
6
|
+
export interface DataSource<C extends Client> {
|
|
7
|
+
readonly key: string;
|
|
8
|
+
/** Construct only; Client.initialize owns external work. */
|
|
9
|
+
createClient(signal: AbortSignal): C;
|
|
10
|
+
}
|
|
11
|
+
/** Resolution owns configuration semantics; transports preserve the resolved target and identity. */
|
|
12
|
+
export interface ConnectionSource<Target> {
|
|
13
|
+
resolve(): Promise<Target>;
|
|
14
|
+
readonly transports: readonly Transport[];
|
|
15
|
+
}
|
|
16
|
+
export interface ClientLifecycle {
|
|
17
|
+
signal?: AbortSignal;
|
|
18
|
+
onDispose?: (dispose: () => Promise<void>) => void;
|
|
19
|
+
/** Safe route facts only: no credentials, SQL or protocol payloads. */
|
|
20
|
+
onRoute?: (route: {
|
|
21
|
+
transport: string;
|
|
22
|
+
reason?: string;
|
|
23
|
+
}) => void;
|
|
24
|
+
}
|
|
25
|
+
/** Stable in-memory identity; connection credentials must not appear in observable cache keys. */
|
|
26
|
+
export declare function dataSourceKey(protocol: string, configuration: unknown): string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { RequestInfo, RequestInit, Response } from "node-fetch";
|
|
2
|
+
import type { ExecResult } from "./executor";
|
|
3
|
+
import type { KubernetesPodLogAccess, PodLogRequest, PodLogResult, ServicePodListResult } from "./pod-log";
|
|
4
|
+
export interface ClientNodeLogPolicy {
|
|
5
|
+
idleTimeoutMs: number;
|
|
6
|
+
hardTimeoutMs: number;
|
|
7
|
+
maxAttempts: number;
|
|
8
|
+
}
|
|
9
|
+
export type ClientNodeFetch = (url: RequestInfo, init?: RequestInit) => Promise<Response>;
|
|
10
|
+
export declare const DEFAULT_CLIENT_NODE_LOG_POLICY: ClientNodeLogPolicy;
|
|
11
|
+
export interface ClientNodePodLogOptions {
|
|
12
|
+
namespace: string;
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
kubeconfig?: string;
|
|
15
|
+
context?: string;
|
|
16
|
+
policy?: Partial<ClientNodeLogPolicy>;
|
|
17
|
+
/** Transport seam for deterministic tests; production uses node-fetch. */
|
|
18
|
+
fetchImpl?: ClientNodeFetch;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 迁移期组合:Service/Pod 发现仍走既有 access,只有高体量 Pod Log transport 改为 API 流。
|
|
22
|
+
* 调用侧不需要知道两种 transport;后续发现层 API 化时可直接替换 delegate。
|
|
23
|
+
*/
|
|
24
|
+
export declare class ClientNodePodLogAccess implements KubernetesPodLogAccess {
|
|
25
|
+
private readonly discovery;
|
|
26
|
+
private readonly transport;
|
|
27
|
+
constructor(discovery: KubernetesPodLogAccess, options: ClientNodePodLogOptions);
|
|
28
|
+
clientVersion(): Promise<ExecResult>;
|
|
29
|
+
listServicePods(services: readonly string[]): Promise<ServicePodListResult>;
|
|
30
|
+
collectPodLogs(request: PodLogRequest): Promise<PodLogResult>;
|
|
31
|
+
}
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import {
|
|
2
|
+
logTimestampNanos
|
|
3
|
+
} from "../chunk-mp88s7r7.js";
|
|
4
|
+
|
|
5
|
+
// src/kubernetes/client-node-pod-log.ts
|
|
6
|
+
import { closeSync, openSync, writeSync } from "node:fs";
|
|
7
|
+
import { KubeConfig } from "@kubernetes/client-node";
|
|
8
|
+
import fetch from "node-fetch/lib/index.js";
|
|
9
|
+
var DEFAULT_CLIENT_NODE_LOG_POLICY = {
|
|
10
|
+
idleTimeoutMs: 15000,
|
|
11
|
+
hardTimeoutMs: 120000,
|
|
12
|
+
maxAttempts: 2
|
|
13
|
+
};
|
|
14
|
+
var RFC3339_LINE = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2}))(?:\s|$)/;
|
|
15
|
+
var RECENT_LINE_LIMIT = 256;
|
|
16
|
+
function parseSinceSeconds(value) {
|
|
17
|
+
const input = value.trim();
|
|
18
|
+
if (!input)
|
|
19
|
+
return;
|
|
20
|
+
const units = {
|
|
21
|
+
ns: 0.000000001,
|
|
22
|
+
us: 0.000001,
|
|
23
|
+
"µs": 0.000001,
|
|
24
|
+
ms: 0.001,
|
|
25
|
+
s: 1,
|
|
26
|
+
m: 60,
|
|
27
|
+
h: 3600
|
|
28
|
+
};
|
|
29
|
+
let seconds = 0;
|
|
30
|
+
let cursor = 0;
|
|
31
|
+
const segment = /(\d+(?:\.\d+)?)(ns|us|µs|ms|s|m|h)/gy;
|
|
32
|
+
while (cursor < input.length) {
|
|
33
|
+
segment.lastIndex = cursor;
|
|
34
|
+
const match = segment.exec(input);
|
|
35
|
+
if (!match)
|
|
36
|
+
return;
|
|
37
|
+
seconds += Number(match[1]) * units[match[2]];
|
|
38
|
+
cursor = segment.lastIndex;
|
|
39
|
+
}
|
|
40
|
+
return Math.max(1, Math.ceil(seconds));
|
|
41
|
+
}
|
|
42
|
+
function errorMessage(error) {
|
|
43
|
+
return error instanceof Error ? error.message : String(error);
|
|
44
|
+
}
|
|
45
|
+
function logCommand(namespace, request) {
|
|
46
|
+
const command = [
|
|
47
|
+
"kubernetes-api",
|
|
48
|
+
"logs",
|
|
49
|
+
"-n",
|
|
50
|
+
namespace,
|
|
51
|
+
request.pod
|
|
52
|
+
];
|
|
53
|
+
if (request.container)
|
|
54
|
+
command.push("-c", request.container);
|
|
55
|
+
if (request.previous)
|
|
56
|
+
command.push("--previous");
|
|
57
|
+
if (request.tail !== undefined)
|
|
58
|
+
command.push(`--tail=${request.tail}`);
|
|
59
|
+
if (request.limitBytes !== undefined)
|
|
60
|
+
command.push(`--limit-bytes=${request.limitBytes}`);
|
|
61
|
+
if (request.sinceTime)
|
|
62
|
+
command.push(`--since-time=${request.sinceTime}`);
|
|
63
|
+
else if (request.since)
|
|
64
|
+
command.push(`--since=${request.since}`);
|
|
65
|
+
if (request.untilTime)
|
|
66
|
+
command.push(`--until-time=${request.untilTime}`);
|
|
67
|
+
return command;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class ClientNodeLogTransport {
|
|
71
|
+
options;
|
|
72
|
+
kubeConfig = new KubeConfig;
|
|
73
|
+
policy;
|
|
74
|
+
initializationError;
|
|
75
|
+
constructor(options) {
|
|
76
|
+
this.options = options;
|
|
77
|
+
this.policy = { ...DEFAULT_CLIENT_NODE_LOG_POLICY, ...options.policy };
|
|
78
|
+
try {
|
|
79
|
+
if (this.policy.idleTimeoutMs < 1 || this.policy.hardTimeoutMs < 1) {
|
|
80
|
+
throw new Error("Pod Log timeout 必须为正数");
|
|
81
|
+
}
|
|
82
|
+
if (!Number.isInteger(this.policy.maxAttempts) || this.policy.maxAttempts < 1) {
|
|
83
|
+
throw new Error("Pod Log maxAttempts 必须是正整数");
|
|
84
|
+
}
|
|
85
|
+
if (options.kubeconfig)
|
|
86
|
+
this.kubeConfig.loadFromFile(options.kubeconfig);
|
|
87
|
+
else
|
|
88
|
+
this.kubeConfig.loadFromDefault();
|
|
89
|
+
if (options.context)
|
|
90
|
+
this.kubeConfig.setCurrentContext(options.context);
|
|
91
|
+
const cluster = this.kubeConfig.getCurrentCluster();
|
|
92
|
+
if (!cluster) {
|
|
93
|
+
throw new Error(`kubeconfig context '${this.kubeConfig.getCurrentContext()}' 没有关联 cluster`);
|
|
94
|
+
}
|
|
95
|
+
if (new URL(cluster.server).protocol === "http:") {
|
|
96
|
+
this.kubeConfig.clusters = this.kubeConfig.clusters.map((item) => item === cluster ? { ...item, skipTLSVerify: true } : item);
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
this.initializationError = errorMessage(error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async capture(request) {
|
|
103
|
+
this.options.signal?.throwIfAborted();
|
|
104
|
+
const command = logCommand(this.options.namespace, request);
|
|
105
|
+
const startedAt = Date.now();
|
|
106
|
+
if (this.initializationError) {
|
|
107
|
+
return this.unavailable(command, startedAt, `kubeconfig 初始化失败:${this.initializationError}`);
|
|
108
|
+
}
|
|
109
|
+
if (!request.container) {
|
|
110
|
+
return this.unavailable(command, startedAt, "Kubernetes Pod Log API 要求指定 container");
|
|
111
|
+
}
|
|
112
|
+
const sinceSeconds = request.since && !request.sinceTime ? parseSinceSeconds(request.since) : undefined;
|
|
113
|
+
if (request.since && !request.sinceTime && sinceSeconds === undefined) {
|
|
114
|
+
return this.unavailable(command, startedAt, `无法解析日志时间窗口:${request.since}`);
|
|
115
|
+
}
|
|
116
|
+
const untilNanos = request.untilTime === undefined ? undefined : logTimestampNanos(request.untilTime);
|
|
117
|
+
if (request.untilTime !== undefined && untilNanos === undefined) {
|
|
118
|
+
return this.unavailable(command, startedAt, "untilTime 必须是 RFC3339 时间戳");
|
|
119
|
+
}
|
|
120
|
+
const fd = request.rawFilePath ? openSync(request.rawFilePath, "w", 384) : undefined;
|
|
121
|
+
const output = [];
|
|
122
|
+
let rawChunks = [];
|
|
123
|
+
let rawBytes = 0;
|
|
124
|
+
const flushRaw = () => {
|
|
125
|
+
if (fd !== undefined && rawChunks.length)
|
|
126
|
+
writeSync(fd, rawChunks.join(""));
|
|
127
|
+
rawChunks = [];
|
|
128
|
+
rawBytes = 0;
|
|
129
|
+
};
|
|
130
|
+
const recentLines = [];
|
|
131
|
+
const recentLineSet = new Set;
|
|
132
|
+
const errors = [];
|
|
133
|
+
let attempts = 0;
|
|
134
|
+
let totalBytesRead = 0;
|
|
135
|
+
let resumeSinceTime = request.sinceTime;
|
|
136
|
+
let timedOut = false;
|
|
137
|
+
let finalStatusCode;
|
|
138
|
+
const hardDeadline = startedAt + this.policy.hardTimeoutMs;
|
|
139
|
+
const emitLine = (line, deduplicate) => {
|
|
140
|
+
if (deduplicate && recentLineSet.has(line))
|
|
141
|
+
return;
|
|
142
|
+
const prefix = request.prefix ? `[pod/${request.pod}/${request.container}] ` : "";
|
|
143
|
+
const rendered = `${prefix}${line}`;
|
|
144
|
+
request.onLine?.(rendered);
|
|
145
|
+
if (fd !== undefined) {
|
|
146
|
+
const chunk = `${rendered}
|
|
147
|
+
`;
|
|
148
|
+
rawChunks.push(chunk);
|
|
149
|
+
rawBytes += Buffer.byteLength(chunk);
|
|
150
|
+
if (rawBytes >= 64 * 1024)
|
|
151
|
+
flushRaw();
|
|
152
|
+
} else if (request.collectStdout !== false)
|
|
153
|
+
output.push(rendered);
|
|
154
|
+
recentLines.push(line);
|
|
155
|
+
recentLineSet.add(line);
|
|
156
|
+
if (recentLines.length > RECENT_LINE_LIMIT) {
|
|
157
|
+
recentLineSet.delete(recentLines.shift());
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
try {
|
|
161
|
+
while (attempts < this.policy.maxAttempts) {
|
|
162
|
+
if (this.options.signal?.aborted)
|
|
163
|
+
break;
|
|
164
|
+
const remainingHardTimeoutMs = hardDeadline - Date.now();
|
|
165
|
+
if (remainingHardTimeoutMs <= 0) {
|
|
166
|
+
timedOut = true;
|
|
167
|
+
errors.push(`hard_timeout(hard=${this.policy.hardTimeoutMs}ms)`);
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
const remainingBytes = request.limitBytes === undefined ? undefined : Math.max(0, request.limitBytes - totalBytesRead);
|
|
171
|
+
if (remainingBytes === 0)
|
|
172
|
+
break;
|
|
173
|
+
attempts += 1;
|
|
174
|
+
const attempt = await this.captureAttempt({
|
|
175
|
+
request: remainingBytes === undefined ? request : { ...request, limitBytes: remainingBytes },
|
|
176
|
+
sinceSeconds,
|
|
177
|
+
untilNanos,
|
|
178
|
+
sinceTime: resumeSinceTime,
|
|
179
|
+
hardTimeoutMs: remainingHardTimeoutMs,
|
|
180
|
+
deduplicate: attempts > 1,
|
|
181
|
+
emitLine
|
|
182
|
+
});
|
|
183
|
+
totalBytesRead += attempt.bytesRead;
|
|
184
|
+
timedOut ||= attempt.timedOut;
|
|
185
|
+
finalStatusCode = attempt.statusCode;
|
|
186
|
+
if (attempt.lastTimestamp)
|
|
187
|
+
resumeSinceTime = attempt.lastTimestamp;
|
|
188
|
+
if (attempt.ok) {
|
|
189
|
+
const limited = !attempt.windowComplete && request.limitBytes !== undefined && totalBytesRead >= request.limitBytes;
|
|
190
|
+
return {
|
|
191
|
+
ok: !limited,
|
|
192
|
+
exitCode: limited ? null : 0,
|
|
193
|
+
stdout: output.length ? `${output.join(`
|
|
194
|
+
`)}
|
|
195
|
+
` : "",
|
|
196
|
+
stderr: limited ? `日志达到单次字节上限 ${request.limitBytes}` : "",
|
|
197
|
+
durationMs: Date.now() - startedAt,
|
|
198
|
+
timedOut,
|
|
199
|
+
command,
|
|
200
|
+
captureStatus: limited ? "partial" : "complete",
|
|
201
|
+
reason: limited ? "byte_limit" : undefined,
|
|
202
|
+
bytesRead: totalBytesRead,
|
|
203
|
+
attempts
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
errors.push(attempt.error ?? "Kubernetes Pod Log API 请求失败");
|
|
207
|
+
if (this.options.signal?.aborted || !attempt.retryable || attempts >= this.policy.maxAttempts)
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
} finally {
|
|
211
|
+
try {
|
|
212
|
+
flushRaw();
|
|
213
|
+
} finally {
|
|
214
|
+
if (fd !== undefined)
|
|
215
|
+
closeSync(fd);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const captureStatus = totalBytesRead > 0 ? "partial" : "unavailable";
|
|
219
|
+
const byteLimitReached = request.limitBytes !== undefined && totalBytesRead >= request.limitBytes;
|
|
220
|
+
if (byteLimitReached)
|
|
221
|
+
errors.push(`日志达到单次字节上限 ${request.limitBytes}`);
|
|
222
|
+
return {
|
|
223
|
+
ok: false,
|
|
224
|
+
exitCode: finalStatusCode ?? null,
|
|
225
|
+
stdout: output.length ? `${output.join(`
|
|
226
|
+
`)}
|
|
227
|
+
` : "",
|
|
228
|
+
stderr: errors.join(`
|
|
229
|
+
`),
|
|
230
|
+
durationMs: Date.now() - startedAt,
|
|
231
|
+
timedOut,
|
|
232
|
+
command,
|
|
233
|
+
captureStatus,
|
|
234
|
+
reason: this.options.signal?.aborted ? "cancelled" : byteLimitReached ? "byte_limit" : timedOut ? "timeout" : "transport_error",
|
|
235
|
+
bytesRead: totalBytesRead,
|
|
236
|
+
attempts
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
unavailable(command, startedAt, reason) {
|
|
240
|
+
return {
|
|
241
|
+
ok: false,
|
|
242
|
+
exitCode: null,
|
|
243
|
+
stdout: "",
|
|
244
|
+
stderr: reason,
|
|
245
|
+
durationMs: Date.now() - startedAt,
|
|
246
|
+
timedOut: false,
|
|
247
|
+
command,
|
|
248
|
+
captureStatus: "unavailable",
|
|
249
|
+
reason,
|
|
250
|
+
bytesRead: 0,
|
|
251
|
+
attempts: 0
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
async captureAttempt(input) {
|
|
255
|
+
const cluster = this.kubeConfig.getCurrentCluster();
|
|
256
|
+
const path = `/api/v1/namespaces/${encodeURIComponent(this.options.namespace)}` + `/pods/${encodeURIComponent(input.request.pod)}/log`;
|
|
257
|
+
const url = new URL(path, cluster.server.endsWith("/") ? cluster.server : `${cluster.server}/`);
|
|
258
|
+
url.searchParams.set("container", input.request.container);
|
|
259
|
+
url.searchParams.set("follow", "false");
|
|
260
|
+
url.searchParams.set("timestamps", "true");
|
|
261
|
+
if (input.request.previous)
|
|
262
|
+
url.searchParams.set("previous", "true");
|
|
263
|
+
if (input.request.tail !== undefined)
|
|
264
|
+
url.searchParams.set("tailLines", String(input.request.tail));
|
|
265
|
+
if (input.request.limitBytes !== undefined) {
|
|
266
|
+
url.searchParams.set("limitBytes", String(input.request.limitBytes));
|
|
267
|
+
}
|
|
268
|
+
if (input.sinceTime)
|
|
269
|
+
url.searchParams.set("sinceTime", input.sinceTime);
|
|
270
|
+
else if (input.sinceSeconds !== undefined) {
|
|
271
|
+
url.searchParams.set("sinceSeconds", String(input.sinceSeconds));
|
|
272
|
+
}
|
|
273
|
+
const controller = new AbortController;
|
|
274
|
+
const commandSignal = this.options.signal;
|
|
275
|
+
const abort = () => controller.abort(commandSignal?.reason);
|
|
276
|
+
commandSignal?.addEventListener("abort", abort, { once: true });
|
|
277
|
+
if (commandSignal?.aborted)
|
|
278
|
+
abort();
|
|
279
|
+
let abortReason;
|
|
280
|
+
let idleTimer;
|
|
281
|
+
const resetIdleTimer = () => {
|
|
282
|
+
clearTimeout(idleTimer);
|
|
283
|
+
idleTimer = setTimeout(() => {
|
|
284
|
+
abortReason = "idle_timeout";
|
|
285
|
+
controller.abort();
|
|
286
|
+
}, this.policy.idleTimeoutMs);
|
|
287
|
+
};
|
|
288
|
+
resetIdleTimer();
|
|
289
|
+
const hardTimer = setTimeout(() => {
|
|
290
|
+
abortReason = "hard_timeout";
|
|
291
|
+
controller.abort();
|
|
292
|
+
}, input.hardTimeoutMs);
|
|
293
|
+
const decoder = new TextDecoder;
|
|
294
|
+
let pending = "";
|
|
295
|
+
let bytesRead = 0;
|
|
296
|
+
let lastTimestamp;
|
|
297
|
+
let windowComplete = false;
|
|
298
|
+
const emit = (line) => {
|
|
299
|
+
const timestamp = line.match(RFC3339_LINE)?.[1];
|
|
300
|
+
const nanos = timestamp && input.untilNanos !== undefined ? logTimestampNanos(timestamp) : undefined;
|
|
301
|
+
if (nanos !== undefined && input.untilNanos !== undefined && nanos > input.untilNanos) {
|
|
302
|
+
windowComplete = true;
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (timestamp)
|
|
306
|
+
lastTimestamp = timestamp;
|
|
307
|
+
input.emitLine(line, input.deduplicate);
|
|
308
|
+
};
|
|
309
|
+
const consume = (text) => {
|
|
310
|
+
pending += text;
|
|
311
|
+
const lines = pending.split(/\r?\n/);
|
|
312
|
+
pending = lines.pop() ?? "";
|
|
313
|
+
for (const line of lines) {
|
|
314
|
+
emit(line);
|
|
315
|
+
if (windowComplete)
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
try {
|
|
320
|
+
const init = await this.kubeConfig.applyToFetchOptions({});
|
|
321
|
+
const response = await (this.options.fetchImpl ?? fetch)(url, {
|
|
322
|
+
...init,
|
|
323
|
+
method: "GET",
|
|
324
|
+
signal: controller.signal
|
|
325
|
+
});
|
|
326
|
+
if (!response.ok) {
|
|
327
|
+
const body = (await response.text()).trim().slice(0, 2000);
|
|
328
|
+
return {
|
|
329
|
+
ok: false,
|
|
330
|
+
retryable: response.status === 408 || response.status === 429 || response.status >= 500,
|
|
331
|
+
timedOut: false,
|
|
332
|
+
error: `Kubernetes Pod Log API HTTP ${response.status}${body ? `: ${body}` : ""}`,
|
|
333
|
+
statusCode: response.status,
|
|
334
|
+
bytesRead
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
if (!response.body) {
|
|
338
|
+
return {
|
|
339
|
+
ok: false,
|
|
340
|
+
retryable: true,
|
|
341
|
+
timedOut: false,
|
|
342
|
+
error: "Kubernetes Pod Log API 返回空响应流",
|
|
343
|
+
statusCode: response.status,
|
|
344
|
+
bytesRead
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
response.body.on("error", () => {
|
|
348
|
+
return;
|
|
349
|
+
});
|
|
350
|
+
for await (const raw of response.body) {
|
|
351
|
+
const chunk = raw instanceof Uint8Array ? raw : Buffer.from(raw);
|
|
352
|
+
bytesRead += chunk.byteLength;
|
|
353
|
+
resetIdleTimer();
|
|
354
|
+
consume(decoder.decode(chunk, { stream: true }));
|
|
355
|
+
if (windowComplete) {
|
|
356
|
+
controller.abort();
|
|
357
|
+
return { ok: true, retryable: false, timedOut: false, bytesRead, lastTimestamp, windowComplete: true };
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
commandSignal?.throwIfAborted();
|
|
361
|
+
consume(decoder.decode());
|
|
362
|
+
if (pending && !windowComplete)
|
|
363
|
+
emit(pending);
|
|
364
|
+
if (abortReason) {
|
|
365
|
+
return {
|
|
366
|
+
ok: false,
|
|
367
|
+
retryable: true,
|
|
368
|
+
timedOut: true,
|
|
369
|
+
error: `${abortReason}(idle=${this.policy.idleTimeoutMs}ms, hard=${this.policy.hardTimeoutMs}ms)`,
|
|
370
|
+
statusCode: response.status,
|
|
371
|
+
bytesRead,
|
|
372
|
+
lastTimestamp
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
ok: true,
|
|
377
|
+
retryable: false,
|
|
378
|
+
timedOut: false,
|
|
379
|
+
statusCode: response.status,
|
|
380
|
+
bytesRead,
|
|
381
|
+
lastTimestamp,
|
|
382
|
+
windowComplete
|
|
383
|
+
};
|
|
384
|
+
} catch (error) {
|
|
385
|
+
if (windowComplete && !commandSignal?.aborted) {
|
|
386
|
+
return { ok: true, retryable: false, timedOut: false, bytesRead, lastTimestamp, windowComplete: true };
|
|
387
|
+
}
|
|
388
|
+
const timedOut = abortReason !== undefined;
|
|
389
|
+
return {
|
|
390
|
+
ok: false,
|
|
391
|
+
retryable: true,
|
|
392
|
+
timedOut,
|
|
393
|
+
error: timedOut ? `${abortReason}(idle=${this.policy.idleTimeoutMs}ms, hard=${this.policy.hardTimeoutMs}ms)` : errorMessage(error),
|
|
394
|
+
bytesRead,
|
|
395
|
+
lastTimestamp
|
|
396
|
+
};
|
|
397
|
+
} finally {
|
|
398
|
+
commandSignal?.removeEventListener("abort", abort);
|
|
399
|
+
clearTimeout(idleTimer);
|
|
400
|
+
clearTimeout(hardTimer);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
class ClientNodePodLogAccess {
|
|
406
|
+
discovery;
|
|
407
|
+
transport;
|
|
408
|
+
constructor(discovery, options) {
|
|
409
|
+
this.discovery = discovery;
|
|
410
|
+
this.transport = new ClientNodeLogTransport(options);
|
|
411
|
+
}
|
|
412
|
+
clientVersion() {
|
|
413
|
+
return this.discovery.clientVersion();
|
|
414
|
+
}
|
|
415
|
+
listServicePods(services) {
|
|
416
|
+
return this.discovery.listServicePods(services);
|
|
417
|
+
}
|
|
418
|
+
collectPodLogs(request) {
|
|
419
|
+
return this.transport.capture(request);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
export {
|
|
423
|
+
DEFAULT_CLIENT_NODE_LOG_POLICY,
|
|
424
|
+
ClientNodePodLogAccess
|
|
425
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Client } from "../client";
|
|
2
|
+
import { type Executor, type KubectlOptions, type ExecTarget, type RunOptions } from "./executor";
|
|
3
|
+
import type { KubernetesEndpoint } from "./endpoint";
|
|
4
|
+
/** Owns cluster transports; protocol callers borrow this client without closing it. */
|
|
5
|
+
export declare class KubernetesClient implements Client {
|
|
6
|
+
#private;
|
|
7
|
+
private readonly kube;
|
|
8
|
+
readonly signal: AbortSignal;
|
|
9
|
+
constructor(kube: KubectlOptions & {
|
|
10
|
+
namespace: string;
|
|
11
|
+
}, signal?: AbortSignal, executor?: Executor);
|
|
12
|
+
initialize(): Promise<void>;
|
|
13
|
+
run(namespace: string, command: string[], options?: RunOptions): Promise<import("./executor").ExecResult>;
|
|
14
|
+
exec(namespace: string, target: ExecTarget, command: string[], options?: RunOptions): Promise<import("./executor").ExecResult>;
|
|
15
|
+
forward(namespace: string, target: KubernetesEndpoint): Promise<import("./service-port-forward").ForwardedEndpoint>;
|
|
16
|
+
dispose(): Promise<void>;
|
|
17
|
+
}
|