@compforge/harness-toolbox 0.3.9 → 0.3.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.
- package/README.md +52 -0
- package/dist/{chunk-zhkrydeg.js → chunk-20kv4wc3.js} +4 -1
- package/dist/{chunk-p6k59m3q.js → chunk-8axd2y2m.js} +1 -1
- package/dist/{chunk-w3a4p6jm.js → chunk-8mb6natq.js} +68 -14
- package/dist/{chunk-fhtbvj0s.js → chunk-9n9z2pty.js} +1 -1
- package/dist/{chunk-y94xq8b4.js → chunk-a1cb44ga.js} +5 -5
- package/dist/chunk-ba883sdp.js +60 -0
- package/dist/{chunk-ahc00qs5.js → chunk-h73h2b7y.js} +5 -5
- package/dist/chunk-mxyvpy79.js +62 -0
- package/dist/chunk-mzdz6wwc.js +91 -0
- package/dist/datasource.js +1 -1
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +4 -2
- package/dist/file/client.d.ts +17 -0
- package/dist/file/client.js +10 -0
- package/dist/file/index.d.ts +5 -0
- package/dist/file/index.js +24 -0
- package/dist/file/local.d.ts +13 -0
- package/dist/file/local.js +11 -0
- package/dist/file/pod.d.ts +31 -0
- package/dist/file/pod.js +11 -0
- package/dist/kubernetes/client.js +4 -4
- package/dist/kubernetes/environment.js +4 -4
- package/dist/kubernetes/pod-log-datasource.js +1 -1
- package/dist/kubernetes/resources.js +2 -2
- package/dist/kubernetes/workload.js +2 -2
- package/dist/mysql/client.js +3 -3
- package/dist/mysql/index.js +12 -12
- package/dist/opensearch/client.d.ts +16 -2
- package/dist/opensearch/client.js +8 -1
- package/dist/opensearch/index.js +13 -6
- package/dist/s3/datasource.js +1 -1
- package/dist/s3/index.js +1 -1
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -16,6 +16,48 @@ claiming snapshot consistency; callers supply cancellation and decide how to per
|
|
|
16
16
|
Protocol adapters have separate entry points; importing lifecycle or transport contracts does
|
|
17
17
|
not load database or Kubernetes drivers.
|
|
18
18
|
|
|
19
|
+
## File sources
|
|
20
|
+
|
|
21
|
+
Use `LocalFileDataSource` for a local file or `PodFileDataSource` for a file in an explicit
|
|
22
|
+
namespace/Pod/container. Both extend Common's `FileDataSource`, so a Service can store them directly
|
|
23
|
+
as DataSources and consumers can recognize files with `instanceof FileDataSource`. No additional
|
|
24
|
+
`id` or `kind` wrapper is required; consumer-owned selection names remain separate from client identity.
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { ClientManager, FileDataSource } from "@compforge/harness-common";
|
|
28
|
+
import { LocalFileDataSource, PodFileDataSource } from "@compforge/harness-toolbox/file";
|
|
29
|
+
|
|
30
|
+
const source = new LocalFileDataSource("./config.yaml", { concurrency: 2 });
|
|
31
|
+
const clients = new ClientManager();
|
|
32
|
+
try {
|
|
33
|
+
if (source instanceof FileDataSource) {
|
|
34
|
+
const file = await clients.get(source);
|
|
35
|
+
const bytes = await file.read({ maxBytes: 1024 * 1024, timeoutMs: 15_000 });
|
|
36
|
+
console.log(source.path, new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
37
|
+
}
|
|
38
|
+
} finally {
|
|
39
|
+
await clients.dispose();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// `environment` is a KubernetesEnvironment (or an equivalent ClientProvider).
|
|
43
|
+
// Resolve Workloads first and create one source per chosen Pod/container.
|
|
44
|
+
const podSource = new PodFileDataSource(environment,
|
|
45
|
+
{ namespace: "app", pod: "worker-abc", container: "worker" },
|
|
46
|
+
"/etc/app/config.yaml", { concurrency: 2 });
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Each read reopens the file; the manager shares clients, not previous file contents. The byte limit
|
|
50
|
+
rejects oversized reads, including a zero-byte limit on a nonempty file. Timeouts include queue time;
|
|
51
|
+
caller cancellation and disposal cancel active reads. Keep the source path and Pod target with any
|
|
52
|
+
persisted evidence. A successful read describes bytes at that location, not the application's parsed
|
|
53
|
+
or effective configuration and not an atomic filesystem snapshot.
|
|
54
|
+
|
|
55
|
+
Local files may be symlinks to regular files. Pod reads require `pods/exec` access and `head -c` in the
|
|
56
|
+
selected container; paths are passed as argv without a shell. A Pod name is an access location, not
|
|
57
|
+
a UID fence; callers needing incarnation guarantees must verify discovery around collection. Pod
|
|
58
|
+
sources borrow Kubernetes access from the root manager and never close it themselves. Unsupported
|
|
59
|
+
commands, missing paths and denied reads fail explicitly; they are not empty file results.
|
|
60
|
+
|
|
19
61
|
## Workload discovery
|
|
20
62
|
|
|
21
63
|
Borrow a KubernetesClient through KubernetesEnvironment and the shared ClientManager lifecycle, then call
|
|
@@ -250,3 +292,13 @@ Disposal closes forwards and the exec stream; EOF closes remote listeners/connec
|
|
|
250
292
|
connection is lost without EOF, a 30-second heartbeat lease bounds remote process lifetime.
|
|
251
293
|
A dead relay invalidates its routes and fails callers; no SQL or other protocol operation is replayed.
|
|
252
294
|
The root should finish or cancel protocol work before disposing the transport.
|
|
295
|
+
|
|
296
|
+
### OpenSearch connection preparation
|
|
297
|
+
|
|
298
|
+
`openOpenSearch` probes the resolved endpoint before returning. It tries the supplied TCP transports
|
|
299
|
+
in order only for connection failures; HTTP authorization/server errors stop preparation, and failed
|
|
300
|
+
reads are never replayed on another route. `onRoute` reports the selected transport and safe fallback
|
|
301
|
+
reason. The caller owns shared transports such as `PodRelayTransport` and their cleanup.
|
|
302
|
+
|
|
303
|
+
Routed HTTPS preserves the original TLS server name. Bun disables HTTP keep-alive because its reused
|
|
304
|
+
socket recovery can replay a failed request below the driver; Node retains connection pooling.
|
|
@@ -12,6 +12,9 @@ class ToolboxError extends Error {
|
|
|
12
12
|
|
|
13
13
|
class KubernetesError extends ToolboxError {
|
|
14
14
|
}
|
|
15
|
+
|
|
16
|
+
class FileError extends ToolboxError {
|
|
17
|
+
}
|
|
15
18
|
function kubernetesHttpError(status) {
|
|
16
19
|
const kind = {
|
|
17
20
|
401: "authentication_failed",
|
|
@@ -38,4 +41,4 @@ function kubernetesCommandError(result) {
|
|
|
38
41
|
return status ? kubernetesHttpError(status) : new KubernetesError("Kubernetes resource request failed", { kind: "operation_failed" });
|
|
39
42
|
}
|
|
40
43
|
|
|
41
|
-
export { ToolboxError, KubernetesError, kubernetesHttpError, kubernetesCommandError };
|
|
44
|
+
export { ToolboxError, KubernetesError, FileError, kubernetesHttpError, kubernetesCommandError };
|
|
@@ -1,15 +1,30 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ToolboxError
|
|
3
|
+
} from "./chunk-20kv4wc3.js";
|
|
4
|
+
import {
|
|
5
|
+
isConnectionNetworkError
|
|
6
|
+
} from "./chunk-h8fwx0k7.js";
|
|
7
|
+
|
|
1
8
|
// src/opensearch/client.ts
|
|
2
|
-
import { Client } from "@opensearch-project/opensearch";
|
|
9
|
+
import { Client, Connection, errors } from "@opensearch-project/opensearch";
|
|
3
10
|
function isOpenSearchReadApi(search) {
|
|
4
11
|
return typeof search.request === "function";
|
|
5
12
|
}
|
|
13
|
+
|
|
14
|
+
class OpenSearchConnection extends Connection {
|
|
15
|
+
buildRequestObject(params) {
|
|
16
|
+
return { ...super.buildRequestObject(params), servername: this.ssl?.servername };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
6
19
|
function createClient(options) {
|
|
7
20
|
return new Client({
|
|
8
21
|
node: options.node,
|
|
22
|
+
Connection: OpenSearchConnection,
|
|
9
23
|
auth: options.auth.username && options.auth.password ? { username: options.auth.username, password: options.auth.password } : undefined,
|
|
10
|
-
ssl: { rejectUnauthorized: false },
|
|
24
|
+
ssl: { rejectUnauthorized: false, servername: options.servername },
|
|
11
25
|
requestTimeout: options.requestTimeoutMs ?? 60000,
|
|
12
|
-
maxRetries: 0
|
|
26
|
+
maxRetries: 0,
|
|
27
|
+
agent: { keepAlive: !process.versions.bun }
|
|
13
28
|
});
|
|
14
29
|
}
|
|
15
30
|
|
|
@@ -36,8 +51,18 @@ class OpenSearchEngine {
|
|
|
36
51
|
});
|
|
37
52
|
return response.body;
|
|
38
53
|
}
|
|
39
|
-
ping() {
|
|
40
|
-
|
|
54
|
+
async ping(signal, timeoutMs) {
|
|
55
|
+
signal?.throwIfAborted();
|
|
56
|
+
const request = this.client.ping({}, timeoutMs === undefined ? undefined : { requestTimeout: timeoutMs });
|
|
57
|
+
const abort = () => {
|
|
58
|
+
request.abort?.();
|
|
59
|
+
};
|
|
60
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
61
|
+
try {
|
|
62
|
+
return await request;
|
|
63
|
+
} finally {
|
|
64
|
+
signal?.removeEventListener("abort", abort);
|
|
65
|
+
}
|
|
41
66
|
}
|
|
42
67
|
close() {
|
|
43
68
|
return this.client.close();
|
|
@@ -46,6 +71,7 @@ class OpenSearchEngine {
|
|
|
46
71
|
|
|
47
72
|
class OpenSearchClient {
|
|
48
73
|
source;
|
|
74
|
+
lifecycle;
|
|
49
75
|
#controller = new AbortController;
|
|
50
76
|
signal;
|
|
51
77
|
#initialization;
|
|
@@ -54,6 +80,7 @@ class OpenSearchClient {
|
|
|
54
80
|
#operations = new Set;
|
|
55
81
|
constructor(source, lifecycle = {}) {
|
|
56
82
|
this.source = source;
|
|
83
|
+
this.lifecycle = lifecycle;
|
|
57
84
|
this.signal = lifecycle.signal ? AbortSignal.any([lifecycle.signal, this.#controller.signal]) : this.#controller.signal;
|
|
58
85
|
}
|
|
59
86
|
initialize() {
|
|
@@ -61,15 +88,42 @@ class OpenSearchClient {
|
|
|
61
88
|
return this.#initialization ??= (async () => {
|
|
62
89
|
const target = await this.source.resolve();
|
|
63
90
|
this.signal.throwIfAborted();
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
91
|
+
const original = new URL(target.node);
|
|
92
|
+
let reason;
|
|
93
|
+
for (const [index, transport] of this.source.transports.entries()) {
|
|
94
|
+
this.signal.throwIfAborted();
|
|
95
|
+
if (transport.kind !== "tcp")
|
|
96
|
+
throw new Error("OpenSearch requires a TCP transport");
|
|
97
|
+
let engine;
|
|
98
|
+
try {
|
|
99
|
+
const endpoint = await transport.connect({
|
|
100
|
+
host: original.hostname,
|
|
101
|
+
port: Number(original.port || (original.protocol === "https:" ? 443 : 80)),
|
|
102
|
+
servername: target.servername ?? original.hostname
|
|
103
|
+
});
|
|
104
|
+
this.signal.throwIfAborted();
|
|
105
|
+
const url = new URL(original);
|
|
106
|
+
url.hostname = endpoint.host;
|
|
107
|
+
url.port = String(endpoint.port);
|
|
108
|
+
engine = new OpenSearchEngine({ ...target, node: url.toString(), servername: endpoint.servername ?? target.servername ?? original.hostname });
|
|
109
|
+
await engine.ping(this.signal, target.connectTimeoutMs ?? target.requestTimeoutMs ?? 1e4);
|
|
110
|
+
this.signal.throwIfAborted();
|
|
111
|
+
this.lifecycle.onRoute?.({ transport: transport.name, reason });
|
|
112
|
+
this.#engine = engine;
|
|
113
|
+
return;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
await engine?.close();
|
|
116
|
+
this.signal.throwIfAborted();
|
|
117
|
+
const network = isConnectionNetworkError(error);
|
|
118
|
+
if (!network && !(error instanceof errors.ConnectionError) && !(error instanceof errors.TimeoutError))
|
|
119
|
+
throw error;
|
|
120
|
+
reason = network ? String(error.code) : error.name;
|
|
121
|
+
if (index === this.source.transports.length - 1) {
|
|
122
|
+
throw new ToolboxError(`OpenSearch connect failed (${reason})`, { kind: "connection_failed", code: reason, cause: error });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
throw new Error("OpenSearch DataSource has no transport");
|
|
73
127
|
})();
|
|
74
128
|
}
|
|
75
129
|
#ready() {
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveWorkload
|
|
3
|
+
} from "./chunk-8axd2y2m.js";
|
|
1
4
|
import {
|
|
2
5
|
ServicePortForwarder
|
|
3
6
|
} from "./chunk-2171tajs.js";
|
|
4
7
|
import {
|
|
5
8
|
resourceAccess
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import {
|
|
8
|
-
resolveWorkload
|
|
9
|
-
} from "./chunk-p6k59m3q.js";
|
|
9
|
+
} from "./chunk-9n9z2pty.js";
|
|
10
10
|
import {
|
|
11
11
|
KubernetesError
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-20kv4wc3.js";
|
|
13
13
|
import {
|
|
14
14
|
KubectlExecutor
|
|
15
15
|
} from "./chunk-hmah9t8e.js";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BoundedFileClient,
|
|
3
|
+
fileClientOptions
|
|
4
|
+
} from "./chunk-mzdz6wwc.js";
|
|
5
|
+
import {
|
|
6
|
+
FileError
|
|
7
|
+
} from "./chunk-20kv4wc3.js";
|
|
8
|
+
|
|
9
|
+
// src/file/local.ts
|
|
10
|
+
import { constants } from "node:fs";
|
|
11
|
+
import { open } from "node:fs/promises";
|
|
12
|
+
import { resolve } from "node:path";
|
|
13
|
+
import { clientKey, FileDataSource } from "@compforge/harness-common";
|
|
14
|
+
class LocalFileClient extends BoundedFileClient {
|
|
15
|
+
async capture(signal, limits, receive) {
|
|
16
|
+
try {
|
|
17
|
+
const file = await open(this.path, constants.O_RDONLY | constants.O_NONBLOCK);
|
|
18
|
+
try {
|
|
19
|
+
signal.throwIfAborted();
|
|
20
|
+
if (!(await file.stat()).isFile()) {
|
|
21
|
+
throw new FileError(`Not a regular file: ${this.path}`, { kind: "unsupported_operation" });
|
|
22
|
+
}
|
|
23
|
+
const stream = file.createReadStream({ autoClose: false, signal, highWaterMark: Math.min(limits.maxBytes + 1, 64 * 1024) });
|
|
24
|
+
for await (const bytes of stream)
|
|
25
|
+
receive(bytes);
|
|
26
|
+
} finally {
|
|
27
|
+
await file.close();
|
|
28
|
+
}
|
|
29
|
+
} catch (cause) {
|
|
30
|
+
signal.throwIfAborted();
|
|
31
|
+
if (cause instanceof FileError)
|
|
32
|
+
throw cause;
|
|
33
|
+
const code = cause?.code;
|
|
34
|
+
throw new FileError(`Cannot read local file: ${this.path}`, {
|
|
35
|
+
kind: code === "ENOENT" || code === "ENOTDIR" ? "resource_not_found" : code === "EACCES" || code === "EPERM" ? "permission_denied" : "operation_failed",
|
|
36
|
+
code,
|
|
37
|
+
cause
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
class LocalFileDataSource extends FileDataSource {
|
|
44
|
+
path;
|
|
45
|
+
clientKey;
|
|
46
|
+
#options;
|
|
47
|
+
constructor(path, options) {
|
|
48
|
+
super();
|
|
49
|
+
if (!path || path.includes("\x00"))
|
|
50
|
+
throw new FileError("File path must be nonempty and contain no NUL", { kind: "invalid_argument" });
|
|
51
|
+
this.path = resolve(path);
|
|
52
|
+
this.#options = fileClientOptions(options);
|
|
53
|
+
this.clientKey = clientKey("local-file", { path: this.path, options: this.#options });
|
|
54
|
+
}
|
|
55
|
+
createClient(_clients, signal) {
|
|
56
|
+
return new LocalFileClient(this.path, this.#options, signal);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { LocalFileClient, LocalFileDataSource };
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
queryMysqlViaPod,
|
|
3
|
+
queryMysqlViaPodBatch
|
|
4
|
+
} from "./chunk-m91t44jh.js";
|
|
1
5
|
import {
|
|
2
6
|
queryReadonlySession,
|
|
3
7
|
validateQueryLimits
|
|
@@ -5,13 +9,9 @@ import {
|
|
|
5
9
|
import {
|
|
6
10
|
destroyMysqlConnection
|
|
7
11
|
} from "./chunk-136egyff.js";
|
|
8
|
-
import {
|
|
9
|
-
queryMysqlViaPod,
|
|
10
|
-
queryMysqlViaPodBatch
|
|
11
|
-
} from "./chunk-m91t44jh.js";
|
|
12
12
|
import {
|
|
13
13
|
ToolboxError
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-20kv4wc3.js";
|
|
15
15
|
import {
|
|
16
16
|
ConcurrencyPool
|
|
17
17
|
} from "./chunk-v3hxvnrc.js";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BoundedFileClient,
|
|
3
|
+
fileClientOptions
|
|
4
|
+
} from "./chunk-mzdz6wwc.js";
|
|
5
|
+
import {
|
|
6
|
+
FileError
|
|
7
|
+
} from "./chunk-20kv4wc3.js";
|
|
8
|
+
|
|
9
|
+
// src/file/pod.ts
|
|
10
|
+
import { posix } from "node:path";
|
|
11
|
+
import { clientKey, FileDataSource } from "@compforge/harness-common";
|
|
12
|
+
class PodFileClient extends BoundedFileClient {
|
|
13
|
+
target;
|
|
14
|
+
acquire;
|
|
15
|
+
#kubernetes;
|
|
16
|
+
constructor(path, target, options, signal, acquire) {
|
|
17
|
+
super(path, options, signal);
|
|
18
|
+
this.target = target;
|
|
19
|
+
this.acquire = acquire;
|
|
20
|
+
}
|
|
21
|
+
async initializeAccess() {
|
|
22
|
+
this.#kubernetes = await this.acquire();
|
|
23
|
+
}
|
|
24
|
+
async capture(signal, limits, receive) {
|
|
25
|
+
const result = await this.#kubernetes.exec(this.target.namespace, this.target, ["head", "-c", String(limits.maxBytes + 1), this.path], { signal, timeoutMs: limits.timeoutMs, collectStdout: false, onStdoutBytes: receive });
|
|
26
|
+
signal.throwIfAborted();
|
|
27
|
+
if (!result.ok)
|
|
28
|
+
throw new FileError(`Cannot read Pod file: ${this.target.namespace}/${this.target.pod}/${this.target.container} ${this.path}`, { kind: result.timedOut ? "timeout" : "operation_failed", code: result.exitCode ?? undefined });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class PodFileDataSource extends FileDataSource {
|
|
33
|
+
kubernetes;
|
|
34
|
+
path;
|
|
35
|
+
target;
|
|
36
|
+
clientKey;
|
|
37
|
+
#options;
|
|
38
|
+
constructor(kubernetes, target, path, options) {
|
|
39
|
+
super();
|
|
40
|
+
this.kubernetes = kubernetes;
|
|
41
|
+
if (!posix.isAbsolute(path) || path.includes("\x00")) {
|
|
42
|
+
throw new FileError("Pod file path must be absolute and contain no NUL", { kind: "invalid_argument" });
|
|
43
|
+
}
|
|
44
|
+
if ([target.namespace, target.pod, target.container].some((value) => !value.trim())) {
|
|
45
|
+
throw new FileError("Pod file namespace, Pod and Container are required", { kind: "invalid_argument" });
|
|
46
|
+
}
|
|
47
|
+
this.path = path;
|
|
48
|
+
this.target = Object.freeze({ namespace: target.namespace, pod: target.pod, container: target.container });
|
|
49
|
+
this.#options = fileClientOptions(options);
|
|
50
|
+
this.clientKey = clientKey("pod-file", {
|
|
51
|
+
access: kubernetes.clientKey,
|
|
52
|
+
target: this.target,
|
|
53
|
+
path: this.path,
|
|
54
|
+
options: this.#options
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
createClient(clients, signal) {
|
|
58
|
+
return new PodFileClient(this.path, this.target, this.#options, signal, () => clients.get(this.kubernetes));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { PodFileClient, PodFileDataSource };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FileError,
|
|
3
|
+
ToolboxError
|
|
4
|
+
} from "./chunk-20kv4wc3.js";
|
|
5
|
+
import {
|
|
6
|
+
ConcurrencyPool
|
|
7
|
+
} from "./chunk-v3hxvnrc.js";
|
|
8
|
+
|
|
9
|
+
// src/file/client.ts
|
|
10
|
+
function fileClientOptions(options) {
|
|
11
|
+
if (!Number.isSafeInteger(options.concurrency) || options.concurrency < 1) {
|
|
12
|
+
throw new FileError("File concurrency must be a positive integer", { kind: "invalid_argument" });
|
|
13
|
+
}
|
|
14
|
+
return Object.freeze({ concurrency: options.concurrency });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class BoundedFileClient {
|
|
18
|
+
path;
|
|
19
|
+
#controller = new AbortController;
|
|
20
|
+
#signal;
|
|
21
|
+
#pool;
|
|
22
|
+
#operations = new Set;
|
|
23
|
+
#ready = false;
|
|
24
|
+
#disposal;
|
|
25
|
+
constructor(path, options, signal) {
|
|
26
|
+
this.path = path;
|
|
27
|
+
this.#pool = new ConcurrencyPool(fileClientOptions(options).concurrency);
|
|
28
|
+
this.#signal = AbortSignal.any([signal, this.#controller.signal]);
|
|
29
|
+
}
|
|
30
|
+
async initialize() {
|
|
31
|
+
this.#signal.throwIfAborted();
|
|
32
|
+
await this.initializeAccess();
|
|
33
|
+
this.#signal.throwIfAborted();
|
|
34
|
+
this.#ready = true;
|
|
35
|
+
}
|
|
36
|
+
async initializeAccess() {}
|
|
37
|
+
async read(options) {
|
|
38
|
+
this.#signal.throwIfAborted();
|
|
39
|
+
if (!this.#ready)
|
|
40
|
+
throw new FileError(`File client is not initialized: ${this.path}`, { kind: "operation_failed" });
|
|
41
|
+
if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0 || options.maxBytes === Number.MAX_SAFE_INTEGER || !Number.isSafeInteger(options.timeoutMs) || options.timeoutMs <= 0 || options.timeoutMs > 2147483647) {
|
|
42
|
+
throw new FileError(`Invalid file read limits: ${this.path}`, { kind: "invalid_argument" });
|
|
43
|
+
}
|
|
44
|
+
const { maxBytes, timeoutMs } = options;
|
|
45
|
+
const controller = new AbortController;
|
|
46
|
+
const signal = AbortSignal.any([this.#signal, controller.signal, ...options.signal ? [options.signal] : []]);
|
|
47
|
+
const timer = setTimeout(() => controller.abort(new FileError(`File read timed out: ${this.path}`, { kind: "timeout" })), timeoutMs);
|
|
48
|
+
const operation = this.#pool.run(async () => {
|
|
49
|
+
const chunks = [];
|
|
50
|
+
let size = 0;
|
|
51
|
+
await this.capture(signal, { maxBytes, timeoutMs }, (bytes) => {
|
|
52
|
+
if (signal.aborted)
|
|
53
|
+
return;
|
|
54
|
+
if (bytes.byteLength > maxBytes - size) {
|
|
55
|
+
controller.abort(new FileError(`File exceeds ${maxBytes} bytes: ${this.path}`, { kind: "limit_exceeded" }));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
chunks.push(Uint8Array.from(bytes));
|
|
59
|
+
size += bytes.byteLength;
|
|
60
|
+
});
|
|
61
|
+
signal.throwIfAborted();
|
|
62
|
+
const result = new Uint8Array(size);
|
|
63
|
+
let offset = 0;
|
|
64
|
+
for (const chunk of chunks) {
|
|
65
|
+
result.set(chunk, offset);
|
|
66
|
+
offset += chunk.byteLength;
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}, signal);
|
|
70
|
+
this.#operations.add(operation);
|
|
71
|
+
try {
|
|
72
|
+
return await operation;
|
|
73
|
+
} catch (cause) {
|
|
74
|
+
signal.throwIfAborted();
|
|
75
|
+
if (cause instanceof ToolboxError)
|
|
76
|
+
throw cause;
|
|
77
|
+
throw new FileError(`Cannot read file: ${this.path}`, { kind: "operation_failed", cause });
|
|
78
|
+
} finally {
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
this.#operations.delete(operation);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
dispose() {
|
|
84
|
+
return this.#disposal ??= (async () => {
|
|
85
|
+
this.#controller.abort(new Error(`File client disposed: ${this.path}`));
|
|
86
|
+
await Promise.allSettled(this.#operations);
|
|
87
|
+
})();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export { fileClientOptions, BoundedFileClient };
|
package/dist/datasource.js
CHANGED
package/dist/errors.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export declare class ToolboxError extends Error {
|
|
|
11
11
|
}
|
|
12
12
|
export declare class KubernetesError extends ToolboxError {
|
|
13
13
|
}
|
|
14
|
+
export declare class FileError extends ToolboxError {
|
|
15
|
+
}
|
|
14
16
|
export declare function kubernetesHttpError(status: number): KubernetesError;
|
|
15
17
|
/**
|
|
16
18
|
* Normalize a failed kubectl resource request at the transport boundary.
|
package/dist/errors.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
|
+
FileError,
|
|
2
3
|
KubernetesError,
|
|
3
4
|
ToolboxError,
|
|
4
5
|
kubernetesCommandError,
|
|
5
6
|
kubernetesHttpError
|
|
6
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-20kv4wc3.js";
|
|
7
8
|
export {
|
|
8
9
|
kubernetesHttpError,
|
|
9
10
|
kubernetesCommandError,
|
|
10
11
|
ToolboxError,
|
|
11
|
-
KubernetesError
|
|
12
|
+
KubernetesError,
|
|
13
|
+
FileError
|
|
12
14
|
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { FileClient, FileReadOptions } from "@compforge/harness-common";
|
|
2
|
+
export interface FileClientOptions {
|
|
3
|
+
concurrency: number;
|
|
4
|
+
}
|
|
5
|
+
export declare function fileClientOptions(options: FileClientOptions): Readonly<FileClientOptions>;
|
|
6
|
+
/** Shared limits and lifecycle; transports stream bytes and own per-read resource cleanup. */
|
|
7
|
+
export declare abstract class BoundedFileClient implements FileClient {
|
|
8
|
+
#private;
|
|
9
|
+
readonly path: string;
|
|
10
|
+
constructor(path: string, options: FileClientOptions, signal: AbortSignal);
|
|
11
|
+
initialize(): Promise<void>;
|
|
12
|
+
protected initializeAccess(): Promise<void>;
|
|
13
|
+
protected abstract capture(signal: AbortSignal, limits: Pick<FileReadOptions, "maxBytes" | "timeoutMs">, receive: (bytes: Uint8Array) => void): Promise<void>;
|
|
14
|
+
/** @spec Limits apply while streaming; a failed read never publishes partial bytes. */
|
|
15
|
+
read(options: FileReadOptions): Promise<Uint8Array>;
|
|
16
|
+
dispose(): Promise<void>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { FileDataSource, type FileClient, type FileReadOptions } from "@compforge/harness-common";
|
|
2
|
+
export { FileError } from "../errors";
|
|
3
|
+
export type { FileClientOptions } from "./client";
|
|
4
|
+
export { LocalFileDataSource, LocalFileClient } from "./local";
|
|
5
|
+
export { PodFileDataSource, PodFileClient, type PodFileTarget } from "./pod";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PodFileClient,
|
|
3
|
+
PodFileDataSource
|
|
4
|
+
} from "../chunk-mxyvpy79.js";
|
|
5
|
+
import {
|
|
6
|
+
LocalFileClient,
|
|
7
|
+
LocalFileDataSource
|
|
8
|
+
} from "../chunk-ba883sdp.js";
|
|
9
|
+
import"../chunk-mzdz6wwc.js";
|
|
10
|
+
import {
|
|
11
|
+
FileError
|
|
12
|
+
} from "../chunk-20kv4wc3.js";
|
|
13
|
+
import"../chunk-v3hxvnrc.js";
|
|
14
|
+
|
|
15
|
+
// src/file/index.ts
|
|
16
|
+
import { FileDataSource } from "@compforge/harness-common";
|
|
17
|
+
export {
|
|
18
|
+
PodFileDataSource,
|
|
19
|
+
PodFileClient,
|
|
20
|
+
LocalFileDataSource,
|
|
21
|
+
LocalFileClient,
|
|
22
|
+
FileError,
|
|
23
|
+
FileDataSource
|
|
24
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { FileDataSource, type ClientManager, type FileReadOptions } from "@compforge/harness-common";
|
|
2
|
+
import { BoundedFileClient, type FileClientOptions } from "./client";
|
|
3
|
+
export declare class LocalFileClient extends BoundedFileClient {
|
|
4
|
+
protected capture(signal: AbortSignal, limits: Pick<FileReadOptions, "maxBytes" | "timeoutMs">, receive: (bytes: Uint8Array) => void): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
/** @spec Resolve relative paths once at declaration; reads reopen the path to observe replaced mounted files. */
|
|
7
|
+
export declare class LocalFileDataSource extends FileDataSource<LocalFileClient> {
|
|
8
|
+
#private;
|
|
9
|
+
readonly path: string;
|
|
10
|
+
readonly clientKey: string;
|
|
11
|
+
constructor(path: string, options: FileClientOptions);
|
|
12
|
+
createClient(_clients: Pick<ClientManager, "get">, signal: AbortSignal): LocalFileClient;
|
|
13
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { FileDataSource, type ClientManager, type ClientProvider, type FileReadOptions } from "@compforge/harness-common";
|
|
2
|
+
import type { KubernetesClient } from "../kubernetes/client";
|
|
3
|
+
import { BoundedFileClient, type FileClientOptions } from "./client";
|
|
4
|
+
export interface PodFileTarget {
|
|
5
|
+
namespace: string;
|
|
6
|
+
pod: string;
|
|
7
|
+
container: string;
|
|
8
|
+
}
|
|
9
|
+
export type PodFileAccess = Pick<KubernetesClient, "initialize" | "dispose" | "exec">;
|
|
10
|
+
export declare class PodFileClient extends BoundedFileClient {
|
|
11
|
+
#private;
|
|
12
|
+
readonly target: Readonly<PodFileTarget>;
|
|
13
|
+
private readonly acquire;
|
|
14
|
+
constructor(path: string, target: Readonly<PodFileTarget>, options: FileClientOptions, signal: AbortSignal, acquire: () => Promise<PodFileAccess>);
|
|
15
|
+
protected initializeAccess(): Promise<void>;
|
|
16
|
+
protected capture(signal: AbortSignal, limits: Pick<FileReadOptions, "maxBytes" | "timeoutMs">, receive: (bytes: Uint8Array) => void): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* @spec One explicit Pod/container/path per source; workload discovery and replica traversal belong to callers.
|
|
20
|
+
* @rule Borrow Kubernetes access from the root manager; file clients never dispose that shared dependency.
|
|
21
|
+
* @see {@link component://../../../docs/toolbox.md}
|
|
22
|
+
*/
|
|
23
|
+
export declare class PodFileDataSource extends FileDataSource<PodFileClient> {
|
|
24
|
+
#private;
|
|
25
|
+
private readonly kubernetes;
|
|
26
|
+
readonly path: string;
|
|
27
|
+
readonly target: Readonly<PodFileTarget>;
|
|
28
|
+
readonly clientKey: string;
|
|
29
|
+
constructor(kubernetes: ClientProvider<PodFileAccess>, target: PodFileTarget, path: string, options: FileClientOptions);
|
|
30
|
+
createClient(clients: Pick<ClientManager, "get">, signal: AbortSignal): PodFileClient;
|
|
31
|
+
}
|
package/dist/file/pod.js
ADDED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
KubernetesClient
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-a1cb44ga.js";
|
|
4
|
+
import"../chunk-8axd2y2m.js";
|
|
4
5
|
import"../chunk-2171tajs.js";
|
|
5
6
|
import"../chunk-j46x7j7c.js";
|
|
6
7
|
import"../chunk-6fnhzhdb.js";
|
|
7
|
-
import"../chunk-
|
|
8
|
-
import"../chunk-
|
|
9
|
-
import"../chunk-zhkrydeg.js";
|
|
8
|
+
import"../chunk-9n9z2pty.js";
|
|
9
|
+
import"../chunk-20kv4wc3.js";
|
|
10
10
|
import"../chunk-v3hxvnrc.js";
|
|
11
11
|
import"../chunk-631vce5z.js";
|
|
12
12
|
import"../chunk-hmah9t8e.js";
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
KubernetesClient
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-a1cb44ga.js";
|
|
4
|
+
import"../chunk-8axd2y2m.js";
|
|
4
5
|
import"../chunk-2171tajs.js";
|
|
5
6
|
import"../chunk-j46x7j7c.js";
|
|
6
7
|
import"../chunk-6fnhzhdb.js";
|
|
7
|
-
import"../chunk-
|
|
8
|
-
import"../chunk-p6k59m3q.js";
|
|
8
|
+
import"../chunk-9n9z2pty.js";
|
|
9
9
|
import {
|
|
10
10
|
clientKey
|
|
11
11
|
} from "../chunk-a9htb6cr.js";
|
|
12
|
-
import"../chunk-
|
|
12
|
+
import"../chunk-20kv4wc3.js";
|
|
13
13
|
import"../chunk-v3hxvnrc.js";
|
|
14
14
|
import"../chunk-631vce5z.js";
|
|
15
15
|
import"../chunk-hmah9t8e.js";
|
package/dist/mysql/client.js
CHANGED
|
@@ -2,11 +2,11 @@ import {
|
|
|
2
2
|
MysqlClient,
|
|
3
3
|
MysqlDatabase,
|
|
4
4
|
openMysql
|
|
5
|
-
} from "../chunk-
|
|
5
|
+
} from "../chunk-h73h2b7y.js";
|
|
6
|
+
import"../chunk-m91t44jh.js";
|
|
6
7
|
import"../chunk-age65q4n.js";
|
|
7
8
|
import"../chunk-136egyff.js";
|
|
8
|
-
import"../chunk-
|
|
9
|
-
import"../chunk-zhkrydeg.js";
|
|
9
|
+
import"../chunk-20kv4wc3.js";
|
|
10
10
|
import"../chunk-v3hxvnrc.js";
|
|
11
11
|
import"../chunk-h8fwx0k7.js";
|
|
12
12
|
import"../chunk-wv4jwhvj.js";
|
package/dist/mysql/index.js
CHANGED
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
parseMysqlEnvTarget
|
|
3
|
-
} from "../chunk-qq77x9ek.js";
|
|
4
|
-
import {
|
|
5
|
-
limitedRows,
|
|
6
|
-
limitedStatement,
|
|
7
|
-
queryLimited
|
|
8
|
-
} from "../chunk-1gyshqd9.js";
|
|
9
|
-
import"../chunk-pfar9cm3.js";
|
|
10
1
|
import {
|
|
11
2
|
withReadScope
|
|
12
3
|
} from "../chunk-4sqvppz7.js";
|
|
@@ -14,11 +5,20 @@ import {
|
|
|
14
5
|
MysqlClient,
|
|
15
6
|
MysqlDatabase,
|
|
16
7
|
openMysql
|
|
17
|
-
} from "../chunk-
|
|
8
|
+
} from "../chunk-h73h2b7y.js";
|
|
9
|
+
import"../chunk-m91t44jh.js";
|
|
18
10
|
import"../chunk-age65q4n.js";
|
|
11
|
+
import {
|
|
12
|
+
parseMysqlEnvTarget
|
|
13
|
+
} from "../chunk-qq77x9ek.js";
|
|
14
|
+
import {
|
|
15
|
+
limitedRows,
|
|
16
|
+
limitedStatement,
|
|
17
|
+
queryLimited
|
|
18
|
+
} from "../chunk-1gyshqd9.js";
|
|
19
19
|
import"../chunk-136egyff.js";
|
|
20
|
-
import"../chunk-
|
|
21
|
-
import"../chunk-
|
|
20
|
+
import"../chunk-pfar9cm3.js";
|
|
21
|
+
import"../chunk-20kv4wc3.js";
|
|
22
22
|
import"../chunk-v3hxvnrc.js";
|
|
23
23
|
import"../chunk-h8fwx0k7.js";
|
|
24
24
|
import"../chunk-wv4jwhvj.js";
|
|
@@ -9,6 +9,10 @@ export interface OpenSearchOptions {
|
|
|
9
9
|
node: string;
|
|
10
10
|
auth: OpenSearchAuth;
|
|
11
11
|
requestTimeoutMs?: number;
|
|
12
|
+
/** Deadline for the initialization probe; query deadlines remain independent. */
|
|
13
|
+
connectTimeoutMs?: number;
|
|
14
|
+
/** Original TLS identity when a transport maps the socket to localhost. */
|
|
15
|
+
servername?: string;
|
|
12
16
|
}
|
|
13
17
|
export interface OpenSearchClientApi {
|
|
14
18
|
count(params: Record<string, unknown>): Promise<{
|
|
@@ -19,7 +23,11 @@ export interface OpenSearchClientApi {
|
|
|
19
23
|
search(params: Record<string, unknown>): Promise<{
|
|
20
24
|
body: SearchResult;
|
|
21
25
|
}>;
|
|
22
|
-
ping(params?: Record<string, unknown
|
|
26
|
+
ping(params?: Record<string, unknown>, options?: {
|
|
27
|
+
requestTimeout?: number;
|
|
28
|
+
}): Promise<unknown> & {
|
|
29
|
+
abort?(): void;
|
|
30
|
+
};
|
|
23
31
|
transport?: {
|
|
24
32
|
request(params: {
|
|
25
33
|
method: string;
|
|
@@ -43,12 +51,18 @@ export declare class OpenSearchEngine implements OpenSearchReadApi {
|
|
|
43
51
|
count(index: string, query: SearchQuery): Promise<number>;
|
|
44
52
|
search(index: string, body: SearchQuery): Promise<SearchResult>;
|
|
45
53
|
request(path: string, query?: SearchQuery): Promise<unknown>;
|
|
46
|
-
ping(): Promise<unknown>;
|
|
54
|
+
ping(signal?: AbortSignal, timeoutMs?: number): Promise<unknown>;
|
|
47
55
|
close(): Promise<void>;
|
|
48
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* @spec Only connection preparation may advance to another Transport; reads are never replayed.
|
|
59
|
+
* @why The official client connects lazily and wraps socket errors, so creating it does not prove reachability.
|
|
60
|
+
* @case id=opensearch_connect_fallback,desc=`Host cannot reach target`,expect=`Probe next TCP route`,forbid=`Replay a failed search`
|
|
61
|
+
*/
|
|
49
62
|
export declare class OpenSearchClient implements ManagedClient, OpenSearchReadApi {
|
|
50
63
|
#private;
|
|
51
64
|
private readonly source;
|
|
65
|
+
private readonly lifecycle;
|
|
52
66
|
readonly signal: AbortSignal;
|
|
53
67
|
constructor(source: ConnectionSource<OpenSearchOptions>, lifecycle?: ClientLifecycle);
|
|
54
68
|
initialize(): Promise<void>;
|
|
@@ -3,7 +3,14 @@ import {
|
|
|
3
3
|
OpenSearchEngine,
|
|
4
4
|
isOpenSearchReadApi,
|
|
5
5
|
openOpenSearch
|
|
6
|
-
} from "../chunk-
|
|
6
|
+
} from "../chunk-8mb6natq.js";
|
|
7
|
+
import"../chunk-20kv4wc3.js";
|
|
8
|
+
import"../chunk-h8fwx0k7.js";
|
|
9
|
+
import"../chunk-wv4jwhvj.js";
|
|
10
|
+
import"../chunk-631vce5z.js";
|
|
11
|
+
import"../chunk-hmah9t8e.js";
|
|
12
|
+
import"../chunk-mn6dwsvh.js";
|
|
13
|
+
import"../chunk-ssj93brx.js";
|
|
7
14
|
export {
|
|
8
15
|
openOpenSearch,
|
|
9
16
|
isOpenSearchReadApi,
|
package/dist/opensearch/index.js
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
OpenSearchClient,
|
|
3
|
-
OpenSearchEngine,
|
|
4
|
-
isOpenSearchReadApi,
|
|
5
|
-
openOpenSearch
|
|
6
|
-
} from "../chunk-w3a4p6jm.js";
|
|
7
1
|
import {
|
|
8
2
|
searchAfterPages
|
|
9
3
|
} from "../chunk-99p0zt4f.js";
|
|
@@ -11,6 +5,19 @@ import {
|
|
|
11
5
|
parseOpenSearchEndpoint
|
|
12
6
|
} from "../chunk-s1sjyem7.js";
|
|
13
7
|
import"../chunk-djk4kmxb.js";
|
|
8
|
+
import {
|
|
9
|
+
OpenSearchClient,
|
|
10
|
+
OpenSearchEngine,
|
|
11
|
+
isOpenSearchReadApi,
|
|
12
|
+
openOpenSearch
|
|
13
|
+
} from "../chunk-8mb6natq.js";
|
|
14
|
+
import"../chunk-20kv4wc3.js";
|
|
15
|
+
import"../chunk-h8fwx0k7.js";
|
|
16
|
+
import"../chunk-wv4jwhvj.js";
|
|
17
|
+
import"../chunk-631vce5z.js";
|
|
18
|
+
import"../chunk-hmah9t8e.js";
|
|
19
|
+
import"../chunk-mn6dwsvh.js";
|
|
20
|
+
import"../chunk-ssj93brx.js";
|
|
14
21
|
export {
|
|
15
22
|
searchAfterPages,
|
|
16
23
|
parseOpenSearchEndpoint,
|
package/dist/s3/datasource.js
CHANGED
package/dist/s3/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@compforge/harness-toolbox",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
4
4
|
"description": "Shared infrastructure clients, transports and bounded data capture",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -88,6 +88,10 @@
|
|
|
88
88
|
"./data-loader": {
|
|
89
89
|
"types": "./dist/data-loader.d.ts",
|
|
90
90
|
"import": "./dist/data-loader.js"
|
|
91
|
+
},
|
|
92
|
+
"./file": {
|
|
93
|
+
"types": "./dist/file/index.d.ts",
|
|
94
|
+
"import": "./dist/file/index.js"
|
|
91
95
|
}
|
|
92
96
|
},
|
|
93
97
|
"files": [
|
|
@@ -95,7 +99,7 @@
|
|
|
95
99
|
"README.md"
|
|
96
100
|
],
|
|
97
101
|
"dependencies": {
|
|
98
|
-
"@compforge/harness-common": "0.3
|
|
102
|
+
"@compforge/harness-common": "0.5.3",
|
|
99
103
|
"@aws-sdk/client-s3": "3.1133.0",
|
|
100
104
|
"@kubernetes/client-node": "1.4.0",
|
|
101
105
|
"@opensearch-project/opensearch": "3.6.0",
|
|
@@ -110,9 +114,6 @@
|
|
|
110
114
|
"@types/node-fetch": "2.6.13",
|
|
111
115
|
"@types/node": "^22"
|
|
112
116
|
},
|
|
113
|
-
"overrides": {
|
|
114
|
-
"@compforge/harness-common": "file:../common"
|
|
115
|
-
},
|
|
116
117
|
"publishConfig": {
|
|
117
118
|
"access": "public"
|
|
118
119
|
},
|