rightsize 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/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +255 -0
- package/dist/backend-docker/backend.d.ts +53 -0
- package/dist/backend-docker/backend.d.ts.map +1 -0
- package/dist/backend-docker/backend.js +336 -0
- package/dist/backend-docker/backend.js.map +1 -0
- package/dist/backend-docker/client.d.ts +82 -0
- package/dist/backend-docker/client.d.ts.map +1 -0
- package/dist/backend-docker/client.js +190 -0
- package/dist/backend-docker/client.js.map +1 -0
- package/dist/backend-docker/frames.d.ts +44 -0
- package/dist/backend-docker/frames.d.ts.map +1 -0
- package/dist/backend-docker/frames.js +105 -0
- package/dist/backend-docker/frames.js.map +1 -0
- package/dist/backend-docker/index.d.ts +5 -0
- package/dist/backend-docker/index.d.ts.map +1 -0
- package/dist/backend-docker/index.js +20 -0
- package/dist/backend-docker/index.js.map +1 -0
- package/dist/backend-docker/json.d.ts +13 -0
- package/dist/backend-docker/json.d.ts.map +1 -0
- package/dist/backend-docker/json.js +47 -0
- package/dist/backend-docker/json.js.map +1 -0
- package/dist/backend-docker/labels.d.ts +11 -0
- package/dist/backend-docker/labels.d.ts.map +1 -0
- package/dist/backend-docker/labels.js +13 -0
- package/dist/backend-docker/labels.js.map +1 -0
- package/dist/backend-docker/port-conflict.d.ts +9 -0
- package/dist/backend-docker/port-conflict.d.ts.map +1 -0
- package/dist/backend-docker/port-conflict.js +12 -0
- package/dist/backend-docker/port-conflict.js.map +1 -0
- package/dist/backend-docker/provider.d.ts +22 -0
- package/dist/backend-docker/provider.d.ts.map +1 -0
- package/dist/backend-docker/provider.js +36 -0
- package/dist/backend-docker/provider.js.map +1 -0
- package/dist/backend-msb/backend.d.ts +169 -0
- package/dist/backend-msb/backend.d.ts.map +1 -0
- package/dist/backend-msb/backend.js +664 -0
- package/dist/backend-msb/backend.js.map +1 -0
- package/dist/backend-msb/commands.d.ts +28 -0
- package/dist/backend-msb/commands.d.ts.map +1 -0
- package/dist/backend-msb/commands.js +67 -0
- package/dist/backend-msb/commands.js.map +1 -0
- package/dist/backend-msb/exec-tunnel.d.ts +33 -0
- package/dist/backend-msb/exec-tunnel.d.ts.map +1 -0
- package/dist/backend-msb/exec-tunnel.js +163 -0
- package/dist/backend-msb/exec-tunnel.js.map +1 -0
- package/dist/backend-msb/follow-replay.d.ts +14 -0
- package/dist/backend-msb/follow-replay.d.ts.map +1 -0
- package/dist/backend-msb/follow-replay.js +20 -0
- package/dist/backend-msb/follow-replay.js.map +1 -0
- package/dist/backend-msb/image-cache.d.ts +25 -0
- package/dist/backend-msb/image-cache.d.ts.map +1 -0
- package/dist/backend-msb/image-cache.js +27 -0
- package/dist/backend-msb/image-cache.js.map +1 -0
- package/dist/backend-msb/index.d.ts +6 -0
- package/dist/backend-msb/index.d.ts.map +1 -0
- package/dist/backend-msb/index.js +21 -0
- package/dist/backend-msb/index.js.map +1 -0
- package/dist/backend-msb/invoke.d.ts +24 -0
- package/dist/backend-msb/invoke.d.ts.map +1 -0
- package/dist/backend-msb/invoke.js +73 -0
- package/dist/backend-msb/invoke.js.map +1 -0
- package/dist/backend-msb/ls-json.d.ts +23 -0
- package/dist/backend-msb/ls-json.d.ts.map +1 -0
- package/dist/backend-msb/ls-json.js +113 -0
- package/dist/backend-msb/ls-json.js.map +1 -0
- package/dist/backend-msb/network-links.d.ts +7 -0
- package/dist/backend-msb/network-links.d.ts.map +1 -0
- package/dist/backend-msb/network-links.js +31 -0
- package/dist/backend-msb/network-links.js.map +1 -0
- package/dist/backend-msb/platform.d.ts +29 -0
- package/dist/backend-msb/platform.d.ts.map +1 -0
- package/dist/backend-msb/platform.js +128 -0
- package/dist/backend-msb/platform.js.map +1 -0
- package/dist/backend-msb/port-conflict.d.ts +10 -0
- package/dist/backend-msb/port-conflict.d.ts.map +1 -0
- package/dist/backend-msb/port-conflict.js +16 -0
- package/dist/backend-msb/port-conflict.js.map +1 -0
- package/dist/backend-msb/provider.d.ts +10 -0
- package/dist/backend-msb/provider.d.ts.map +1 -0
- package/dist/backend-msb/provider.js +40 -0
- package/dist/backend-msb/provider.js.map +1 -0
- package/dist/backend-msb/provisioner.d.ts +19 -0
- package/dist/backend-msb/provisioner.d.ts.map +1 -0
- package/dist/backend-msb/provisioner.js +332 -0
- package/dist/backend-msb/provisioner.js.map +1 -0
- package/dist/backend-msb/reaper.d.ts +10 -0
- package/dist/backend-msb/reaper.d.ts.map +1 -0
- package/dist/backend-msb/reaper.js +73 -0
- package/dist/backend-msb/reaper.js.map +1 -0
- package/dist/core/backend.d.ts +98 -0
- package/dist/core/backend.d.ts.map +1 -0
- package/dist/core/backend.js +2 -0
- package/dist/core/backend.js.map +1 -0
- package/dist/core/backends.d.ts +37 -0
- package/dist/core/backends.d.ts.map +1 -0
- package/dist/core/backends.js +127 -0
- package/dist/core/backends.js.map +1 -0
- package/dist/core/cleanup.d.ts +37 -0
- package/dist/core/cleanup.d.ts.map +1 -0
- package/dist/core/cleanup.js +86 -0
- package/dist/core/cleanup.js.map +1 -0
- package/dist/core/errors.d.ts +51 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +66 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/free-ports.d.ts +23 -0
- package/dist/core/free-ports.d.ts.map +1 -0
- package/dist/core/free-ports.js +63 -0
- package/dist/core/free-ports.js.map +1 -0
- package/dist/core/generic-container.d.ts +145 -0
- package/dist/core/generic-container.d.ts.map +1 -0
- package/dist/core/generic-container.js +385 -0
- package/dist/core/generic-container.js.map +1 -0
- package/dist/core/model.d.ts +60 -0
- package/dist/core/model.d.ts.map +1 -0
- package/dist/core/model.js +2 -0
- package/dist/core/model.js.map +1 -0
- package/dist/core/mountable-file.d.ts +25 -0
- package/dist/core/mountable-file.d.ts.map +1 -0
- package/dist/core/mountable-file.js +64 -0
- package/dist/core/mountable-file.js.map +1 -0
- package/dist/core/network.d.ts +58 -0
- package/dist/core/network.d.ts.map +1 -0
- package/dist/core/network.js +84 -0
- package/dist/core/network.js.map +1 -0
- package/dist/core/run-id.d.ts +18 -0
- package/dist/core/run-id.d.ts.map +1 -0
- package/dist/core/run-id.js +18 -0
- package/dist/core/run-id.js.map +1 -0
- package/dist/core/wait.d.ts +59 -0
- package/dist/core/wait.d.ts.map +1 -0
- package/dist/core/wait.js +239 -0
- package/dist/core/wait.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/modules/arango.d.ts +14 -0
- package/dist/modules/arango.d.ts.map +1 -0
- package/dist/modules/arango.js +37 -0
- package/dist/modules/arango.js.map +1 -0
- package/dist/modules/clickhouse.d.ts +37 -0
- package/dist/modules/clickhouse.d.ts.map +1 -0
- package/dist/modules/clickhouse.js +69 -0
- package/dist/modules/clickhouse.js.map +1 -0
- package/dist/modules/flink.d.ts +65 -0
- package/dist/modules/flink.d.ts.map +1 -0
- package/dist/modules/flink.js +125 -0
- package/dist/modules/flink.js.map +1 -0
- package/dist/modules/floci.d.ts +46 -0
- package/dist/modules/floci.d.ts.map +1 -0
- package/dist/modules/floci.js +62 -0
- package/dist/modules/floci.js.map +1 -0
- package/dist/modules/index.d.ts +31 -0
- package/dist/modules/index.d.ts.map +1 -0
- package/dist/modules/index.js +31 -0
- package/dist/modules/index.js.map +1 -0
- package/dist/modules/kafka.d.ts +11 -0
- package/dist/modules/kafka.d.ts.map +1 -0
- package/dist/modules/kafka.js +41 -0
- package/dist/modules/kafka.js.map +1 -0
- package/dist/modules/keycloak.d.ts +40 -0
- package/dist/modules/keycloak.d.ts.map +1 -0
- package/dist/modules/keycloak.js +67 -0
- package/dist/modules/keycloak.js.map +1 -0
- package/dist/modules/mariadb.d.ts +56 -0
- package/dist/modules/mariadb.d.ts.map +1 -0
- package/dist/modules/mariadb.js +89 -0
- package/dist/modules/mariadb.js.map +1 -0
- package/dist/modules/memcached.d.ts +24 -0
- package/dist/modules/memcached.d.ts.map +1 -0
- package/dist/modules/memcached.js +105 -0
- package/dist/modules/memcached.js.map +1 -0
- package/dist/modules/mongodb.d.ts +19 -0
- package/dist/modules/mongodb.d.ts.map +1 -0
- package/dist/modules/mongodb.js +69 -0
- package/dist/modules/mongodb.js.map +1 -0
- package/dist/modules/mysql.d.ts +61 -0
- package/dist/modules/mysql.d.ts.map +1 -0
- package/dist/modules/mysql.js +101 -0
- package/dist/modules/mysql.js.map +1 -0
- package/dist/modules/neo4j.d.ts +60 -0
- package/dist/modules/neo4j.d.ts.map +1 -0
- package/dist/modules/neo4j.js +83 -0
- package/dist/modules/neo4j.js.map +1 -0
- package/dist/modules/pinot.d.ts +30 -0
- package/dist/modules/pinot.d.ts.map +1 -0
- package/dist/modules/pinot.js +49 -0
- package/dist/modules/pinot.js.map +1 -0
- package/dist/modules/postgres.d.ts +29 -0
- package/dist/modules/postgres.d.ts.map +1 -0
- package/dist/modules/postgres.js +73 -0
- package/dist/modules/postgres.js.map +1 -0
- package/dist/modules/rabbitmq.d.ts +46 -0
- package/dist/modules/rabbitmq.d.ts.map +1 -0
- package/dist/modules/rabbitmq.js +68 -0
- package/dist/modules/rabbitmq.js.map +1 -0
- package/dist/modules/redis.d.ts +15 -0
- package/dist/modules/redis.d.ts.map +1 -0
- package/dist/modules/redis.js +24 -0
- package/dist/modules/redis.js.map +1 -0
- package/dist/modules/redpanda.d.ts +27 -0
- package/dist/modules/redpanda.d.ts.map +1 -0
- package/dist/modules/redpanda.js +58 -0
- package/dist/modules/redpanda.js.map +1 -0
- package/dist/modules/spring-cloud-config.d.ts +18 -0
- package/dist/modules/spring-cloud-config.d.ts.map +1 -0
- package/dist/modules/spring-cloud-config.js +28 -0
- package/dist/modules/spring-cloud-config.js.map +1 -0
- package/dist/modules/wiremock.d.ts +21 -0
- package/dist/modules/wiremock.d.ts.map +1 -0
- package/dist/modules/wiremock.js +32 -0
- package/dist/modules/wiremock.js.map +1 -0
- package/package.json +104 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { BackendError, PortBindConflictError } from "../core/errors.js";
|
|
3
|
+
import { RunId } from "../core/run-id.js";
|
|
4
|
+
import { DockerClient } from "./client.js";
|
|
5
|
+
import { FrameDemuxer, LineAssembler } from "./frames.js";
|
|
6
|
+
import { extractIds, extractNumber, extractString } from "./json.js";
|
|
7
|
+
import { isPortBindConflictMessage } from "./port-conflict.js";
|
|
8
|
+
import { labelFilterQuery, RUN_ID_LABEL_KEY } from "./labels.js";
|
|
9
|
+
const STOP_TIMEOUT_SECS = 10;
|
|
10
|
+
function encodeQueryValue(s) {
|
|
11
|
+
return encodeURIComponent(s);
|
|
12
|
+
}
|
|
13
|
+
/** `image` split into `[repository, tag]` for `POST /images/create?fromImage=&tag=`; a tag-less reference defaults to `latest`, matching Docker's own convention. */
|
|
14
|
+
function splitRepoTag(image) {
|
|
15
|
+
if (image.includes("@")) {
|
|
16
|
+
return [image, ""];
|
|
17
|
+
}
|
|
18
|
+
// The tag separator is the LAST colon after the last slash, so a registry
|
|
19
|
+
// host:port prefix (`localhost:5000/redis`) isn't mistaken for one.
|
|
20
|
+
const slashIdx = image.lastIndexOf("/") + 1;
|
|
21
|
+
const relColon = image.slice(slashIdx).lastIndexOf(":");
|
|
22
|
+
if (relColon === -1) {
|
|
23
|
+
return [image, "latest"];
|
|
24
|
+
}
|
|
25
|
+
const colon = slashIdx + relColon;
|
|
26
|
+
return [image.slice(0, colon), image.slice(colon + 1)];
|
|
27
|
+
}
|
|
28
|
+
/** Builds the `POST /containers/create` JSON body: port bindings pinned to `127.0.0.1`, read-only/read-write binds, the `host.docker.internal` extra host, and the run-id label. */
|
|
29
|
+
function buildCreateBody(spec) {
|
|
30
|
+
const exposedPorts = {};
|
|
31
|
+
const portBindings = {};
|
|
32
|
+
for (const p of spec.ports) {
|
|
33
|
+
const key = `${p.guestPort}/tcp`;
|
|
34
|
+
exposedPorts[key] = {};
|
|
35
|
+
portBindings[key] = [{ HostIp: "127.0.0.1", HostPort: String(p.hostPort) }];
|
|
36
|
+
}
|
|
37
|
+
const binds = spec.mounts.map((m) => `${m.hostPath}:${m.guestPath}:${m.readOnly ? "ro" : "rw"}`);
|
|
38
|
+
const body = {
|
|
39
|
+
Image: spec.image,
|
|
40
|
+
Env: spec.env.map(([k, v]) => `${k}=${v}`),
|
|
41
|
+
ExposedPorts: exposedPorts,
|
|
42
|
+
Labels: { [RUN_ID_LABEL_KEY]: spec.runId },
|
|
43
|
+
HostConfig: {
|
|
44
|
+
PortBindings: portBindings,
|
|
45
|
+
Binds: binds,
|
|
46
|
+
ExtraHosts: ["host.docker.internal:host-gateway"],
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
if (spec.command !== undefined) {
|
|
50
|
+
body.Cmd = [...spec.command];
|
|
51
|
+
}
|
|
52
|
+
if (spec.memoryLimitMb !== undefined) {
|
|
53
|
+
body.HostConfig.Memory = spec.memoryLimitMb * 1024 * 1024;
|
|
54
|
+
}
|
|
55
|
+
return body;
|
|
56
|
+
}
|
|
57
|
+
async function drainFrames(body, onStdout, onStderr) {
|
|
58
|
+
const demuxer = new FrameDemuxer();
|
|
59
|
+
for await (const chunk of body) {
|
|
60
|
+
const frames = demuxer.push(chunk);
|
|
61
|
+
for (const frame of frames) {
|
|
62
|
+
if (frame.streamType === "stdout") {
|
|
63
|
+
onStdout(frame.payload);
|
|
64
|
+
}
|
|
65
|
+
else if (frame.streamType === "stderr") {
|
|
66
|
+
onStderr(frame.payload);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Drives the Docker daemon over `DockerClient`. This is also the
|
|
73
|
+
* correctness oracle other backends are checked against, since Docker
|
|
74
|
+
* enforces semantics (read-only mounts, native networks) microsandbox only
|
|
75
|
+
* emulates.
|
|
76
|
+
*
|
|
77
|
+
* Unlike the msb backend, a `SandboxHandle` here carries no companion
|
|
78
|
+
* mutable state keyed by id: every operation is a stateless HTTP call
|
|
79
|
+
* against the daemon-assigned container id already on the handle, so
|
|
80
|
+
* there's nothing to look up in a side table.
|
|
81
|
+
*/
|
|
82
|
+
export class DockerBackend {
|
|
83
|
+
client;
|
|
84
|
+
/** `"docker"` — matched against `RIGHTSIZE_BACKEND`. */
|
|
85
|
+
name = "docker";
|
|
86
|
+
supportsNativeNetworks = true;
|
|
87
|
+
networkIds = new Map();
|
|
88
|
+
constructor(client) {
|
|
89
|
+
this.client = client;
|
|
90
|
+
}
|
|
91
|
+
/** Exposed for the transport regression test: the socket path this backend's client actually dials. */
|
|
92
|
+
socketPathForTest() {
|
|
93
|
+
return this.client.getSocketPath();
|
|
94
|
+
}
|
|
95
|
+
async pullIfMissing(image) {
|
|
96
|
+
const inspectPath = `/images/${encodeQueryValue(image)}/json`;
|
|
97
|
+
const inspect = await this.client.request("GET", inspectPath);
|
|
98
|
+
if (inspect.status === 200) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const [repo, tag] = splitRepoTag(image);
|
|
102
|
+
const pullPath = `/images/create?fromImage=${encodeQueryValue(repo)}&tag=${encodeQueryValue(tag)}`;
|
|
103
|
+
const resp = await this.client.request("POST", pullPath);
|
|
104
|
+
if (resp.status >= 400) {
|
|
105
|
+
throw new BackendError(`docker could not pull image '${image}' (HTTP ${resp.status}): ${resp.body.toString()}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async connectNetwork(containerId, networkId, aliases) {
|
|
109
|
+
const body = JSON.stringify({ Container: containerId, EndpointConfig: { Aliases: [...aliases] } });
|
|
110
|
+
const path = `/networks/${networkId}/connect`;
|
|
111
|
+
const resp = await this.client.request("POST", path, body);
|
|
112
|
+
if (resp.status >= 400) {
|
|
113
|
+
throw new BackendError(`docker could not connect container ${containerId} to network ${networkId} (HTTP ${resp.status}): ${resp.body.toString()}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async ensureNetworkGetId(networkId) {
|
|
117
|
+
const cached = this.networkIds.get(networkId);
|
|
118
|
+
if (cached !== undefined) {
|
|
119
|
+
return cached;
|
|
120
|
+
}
|
|
121
|
+
const filters = JSON.stringify({ name: [networkId] });
|
|
122
|
+
const listPath = `/networks?filters=${encodeQueryValue(filters)}`;
|
|
123
|
+
const list = await this.client.request("GET", listPath);
|
|
124
|
+
if (list.status === 200) {
|
|
125
|
+
const ids = extractIds(list.body.toString());
|
|
126
|
+
if (ids.length > 0 && ids[0] !== undefined) {
|
|
127
|
+
this.networkIds.set(networkId, ids[0]);
|
|
128
|
+
return ids[0];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const createBody = JSON.stringify({ Name: networkId });
|
|
132
|
+
const created = await this.client.request("POST", "/networks/create", createBody);
|
|
133
|
+
if (created.status >= 400) {
|
|
134
|
+
throw new BackendError(`docker could not create network '${networkId}' (HTTP ${created.status}): ${created.body.toString()}`);
|
|
135
|
+
}
|
|
136
|
+
const id = extractString(created.body.toString(), "Id");
|
|
137
|
+
if (id === undefined) {
|
|
138
|
+
throw new BackendError(`docker's network-create response for '${networkId}' had no Id field (body: ${created.body.toString()})`);
|
|
139
|
+
}
|
|
140
|
+
this.networkIds.set(networkId, id);
|
|
141
|
+
return id;
|
|
142
|
+
}
|
|
143
|
+
async create(spec) {
|
|
144
|
+
await this.pullIfMissing(spec.image);
|
|
145
|
+
const body = JSON.stringify(buildCreateBody(spec));
|
|
146
|
+
const path = `/containers/create?name=${encodeQueryValue(spec.name)}`;
|
|
147
|
+
const resp = await this.client.request("POST", path, body);
|
|
148
|
+
if (resp.status >= 400) {
|
|
149
|
+
throw new BackendError(`docker could not create container '${spec.name}' (HTTP ${resp.status}): ${resp.body.toString()}`);
|
|
150
|
+
}
|
|
151
|
+
const id = extractString(resp.body.toString(), "Id");
|
|
152
|
+
if (id === undefined) {
|
|
153
|
+
throw new BackendError(`docker's container-create response for '${spec.name}' had no Id field (body: ${resp.body.toString()})`);
|
|
154
|
+
}
|
|
155
|
+
if (spec.networkId !== undefined) {
|
|
156
|
+
const daemonNetworkId = await this.ensureNetworkGetId(spec.networkId);
|
|
157
|
+
await this.connectNetwork(id, daemonNetworkId, spec.aliases);
|
|
158
|
+
}
|
|
159
|
+
return { id, spec };
|
|
160
|
+
}
|
|
161
|
+
async start(handle) {
|
|
162
|
+
const path = `/containers/${handle.id}/start`;
|
|
163
|
+
const resp = await this.client.request("POST", path);
|
|
164
|
+
if (resp.status === 204 || resp.status === 304) {
|
|
165
|
+
return; // 304 = already started; treated as success like the daemon intends.
|
|
166
|
+
}
|
|
167
|
+
const message = resp.body.toString();
|
|
168
|
+
if (resp.status === 500 && isPortBindConflictMessage(message)) {
|
|
169
|
+
throw new PortBindConflictError(`docker could not bind a host port for ${handle.id}: ${message}`);
|
|
170
|
+
}
|
|
171
|
+
throw new BackendError(`docker could not start container ${handle.id} (HTTP ${resp.status}): ${message}`);
|
|
172
|
+
}
|
|
173
|
+
async stop(handle) {
|
|
174
|
+
const path = `/containers/${handle.id}/stop?t=${STOP_TIMEOUT_SECS}`;
|
|
175
|
+
await this.client.request("POST", path).catch(() => { }); // best-effort
|
|
176
|
+
}
|
|
177
|
+
async remove(handle) {
|
|
178
|
+
const path = `/containers/${handle.id}?force=true`;
|
|
179
|
+
await this.client.request("DELETE", path).catch(() => { }); // best-effort
|
|
180
|
+
}
|
|
181
|
+
async exec(handle, cmd) {
|
|
182
|
+
const createBody = JSON.stringify({ AttachStdout: true, AttachStderr: true, Cmd: [...cmd] });
|
|
183
|
+
const createPath = `/containers/${handle.id}/exec`;
|
|
184
|
+
const created = await this.client.request("POST", createPath, createBody);
|
|
185
|
+
if (created.status >= 400) {
|
|
186
|
+
throw new BackendError(`docker could not create an exec for container ${handle.id} (HTTP ${created.status}): ${created.body.toString()}`);
|
|
187
|
+
}
|
|
188
|
+
const execId = extractString(created.body.toString(), "Id");
|
|
189
|
+
if (execId === undefined) {
|
|
190
|
+
throw new BackendError(`docker's exec-create response for container ${handle.id} had no Id field (body: ${created.body.toString()})`);
|
|
191
|
+
}
|
|
192
|
+
const startPath = `/exec/${execId}/start`;
|
|
193
|
+
const startBody = JSON.stringify({ Detach: false });
|
|
194
|
+
const { status, body } = await this.client.requestStream("POST", startPath, startBody);
|
|
195
|
+
if (status >= 400) {
|
|
196
|
+
throw new BackendError(`docker could not start exec ${execId} for container ${handle.id} (HTTP ${status})`);
|
|
197
|
+
}
|
|
198
|
+
const stdoutChunks = [];
|
|
199
|
+
const stderrChunks = [];
|
|
200
|
+
await drainFrames(body, (p) => stdoutChunks.push(p), (p) => stderrChunks.push(p));
|
|
201
|
+
const inspectPath = `/exec/${execId}/json`;
|
|
202
|
+
const inspected = await this.client.request("GET", inspectPath);
|
|
203
|
+
const exitCode = extractNumber(inspected.body.toString(), "ExitCode") ?? -1;
|
|
204
|
+
return {
|
|
205
|
+
exitCode,
|
|
206
|
+
stdout: Buffer.concat(stdoutChunks).toString(),
|
|
207
|
+
stderr: Buffer.concat(stderrChunks).toString(),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
async logs(handle) {
|
|
211
|
+
const path = `/containers/${handle.id}/logs?stdout=1&stderr=1&tail=1000`;
|
|
212
|
+
const { status, body } = await this.client.requestStream("GET", path);
|
|
213
|
+
if (status >= 400) {
|
|
214
|
+
throw new BackendError(`docker could not fetch logs for container ${handle.id} (HTTP ${status})`);
|
|
215
|
+
}
|
|
216
|
+
const assembler = new LineAssembler();
|
|
217
|
+
let out = "";
|
|
218
|
+
await drainFrames(body, (p) => {
|
|
219
|
+
for (const line of assembler.feed(p.toString())) {
|
|
220
|
+
out += line + "\n";
|
|
221
|
+
}
|
|
222
|
+
}, (p) => {
|
|
223
|
+
for (const line of assembler.feed(p.toString())) {
|
|
224
|
+
out += line + "\n";
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
const tail = assembler.flush();
|
|
228
|
+
if (tail !== undefined) {
|
|
229
|
+
out += tail + "\n";
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
/** Docker's stream ends cleanly on its own once the workload stops — unlike msb's `logs -f`, so no watchdog is needed here. */
|
|
234
|
+
async followLogs(handle, consumer) {
|
|
235
|
+
const path = `/containers/${handle.id}/logs?stdout=1&stderr=1&follow=1&tail=all`;
|
|
236
|
+
const { status, body } = await this.client.requestStream("GET", path);
|
|
237
|
+
if (status >= 400) {
|
|
238
|
+
throw new BackendError(`docker could not follow logs for container ${handle.id} (HTTP ${status})`);
|
|
239
|
+
}
|
|
240
|
+
let closeRequested = false;
|
|
241
|
+
const demuxer = new FrameDemuxer();
|
|
242
|
+
const assembler = new LineAssembler();
|
|
243
|
+
const readerDone = (async () => {
|
|
244
|
+
try {
|
|
245
|
+
for await (const chunk of body) {
|
|
246
|
+
if (closeRequested) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
for (const frame of demuxer.push(chunk)) {
|
|
250
|
+
if (frame.streamType !== "stdout" && frame.streamType !== "stderr") {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
for (const line of assembler.feed(frame.payload.toString())) {
|
|
254
|
+
if (closeRequested) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
consumer(line);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
// Best-effort: a stream error just ends delivery.
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (closeRequested) {
|
|
267
|
+
return; // stop delivery, never flush — an explicit close beat the stream to its own end.
|
|
268
|
+
}
|
|
269
|
+
const tail = assembler.flush();
|
|
270
|
+
if (tail !== undefined) {
|
|
271
|
+
consumer(tail);
|
|
272
|
+
}
|
|
273
|
+
})();
|
|
274
|
+
return {
|
|
275
|
+
close: async () => {
|
|
276
|
+
closeRequested = true;
|
|
277
|
+
body.destroy();
|
|
278
|
+
await readerDone.catch(() => { });
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
/** No-op: docker relies entirely on native networks (`ensureNetwork`/`create`'s connect step) — there is nothing to emulate here, unlike msb's exec-tunnel links. */
|
|
283
|
+
async installNetworkLinks(_handle, _links) { }
|
|
284
|
+
async ensureNetwork(networkId) {
|
|
285
|
+
await this.ensureNetworkGetId(networkId);
|
|
286
|
+
}
|
|
287
|
+
async removeNetwork(networkId) {
|
|
288
|
+
const daemonId = this.networkIds.get(networkId);
|
|
289
|
+
this.networkIds.delete(networkId);
|
|
290
|
+
if (daemonId !== undefined) {
|
|
291
|
+
await this.client.request("DELETE", `/networks/${daemonId}`).catch(() => { }); // best-effort
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/** Removes every container carrying this run's label — the reaper safety net's normal-exit counterpart. */
|
|
295
|
+
async close() {
|
|
296
|
+
const filters = labelFilterQuery(RunId.value);
|
|
297
|
+
const path = `/containers/json?all=true&filters=${encodeQueryValue(filters)}`;
|
|
298
|
+
const listed = await this.client.request("GET", path).catch(() => undefined);
|
|
299
|
+
if (listed === undefined || listed.status !== 200) {
|
|
300
|
+
return; // best-effort: nothing more to do if even listing fails.
|
|
301
|
+
}
|
|
302
|
+
const ids = extractIds(listed.body.toString());
|
|
303
|
+
for (const id of ids) {
|
|
304
|
+
await this.client.request("DELETE", `/containers/${id}?force=true`).catch(() => { });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Synchronous, blocking teardown for the process-exit path. Node has no
|
|
309
|
+
* synchronous HTTP client, so this shells out to `curl --unix-socket` via
|
|
310
|
+
* `child_process.spawnSync` — curl ships on macOS and virtually every
|
|
311
|
+
* Linux CI image, and `spawnSync` genuinely blocks the exiting process the
|
|
312
|
+
* way the `"exit"` handler requires. If curl is unavailable, this is a
|
|
313
|
+
* silent no-op: the label-scoped orphan reaper each backend runs at
|
|
314
|
+
* startup (`close()`'s own reaping plus the next run's sweep) is the real
|
|
315
|
+
* safety net for that case, not this best-effort fast path.
|
|
316
|
+
*/
|
|
317
|
+
cleanupSync(id) {
|
|
318
|
+
const socketPath = this.client.getSocketPath();
|
|
319
|
+
try {
|
|
320
|
+
spawnSync("curl", [
|
|
321
|
+
"--silent",
|
|
322
|
+
"--max-time",
|
|
323
|
+
"5",
|
|
324
|
+
"--unix-socket",
|
|
325
|
+
socketPath,
|
|
326
|
+
"-X",
|
|
327
|
+
"DELETE",
|
|
328
|
+
`http://localhost/containers/${id}?force=true`,
|
|
329
|
+
]);
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
// Best-effort — see the doc above.
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
//# sourceMappingURL=backend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backend.js","sourceRoot":"","sources":["../../src/backend-docker/backend.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { IncomingMessage } from "node:http";
|
|
2
|
+
/**
|
|
3
|
+
* Bounds how many header bytes this client will accumulate before giving up.
|
|
4
|
+
* A well-behaved daemon's status line + header block is at most a few
|
|
5
|
+
* hundred bytes; `node:http` already enforces its own such limit internally,
|
|
6
|
+
* but this constant documents the ceiling this backend would apply if it
|
|
7
|
+
* were parsing headers by hand — kept here as the single source of truth
|
|
8
|
+
* other backend code can cite.
|
|
9
|
+
*/
|
|
10
|
+
export declare const MAX_HEADER_BYTES: number;
|
|
11
|
+
/** A fully-buffered daemon response, returned by `DockerClient.request`. */
|
|
12
|
+
export interface DockerResponse {
|
|
13
|
+
/** The HTTP status code. */
|
|
14
|
+
readonly status: number;
|
|
15
|
+
/** The full response body. */
|
|
16
|
+
readonly body: Buffer;
|
|
17
|
+
}
|
|
18
|
+
/** A daemon response whose body is left as a live stream, returned by `DockerClient.requestStream` for `exec`/`logs` calls. */
|
|
19
|
+
export interface DockerStreamResponse {
|
|
20
|
+
/** The HTTP status code. */
|
|
21
|
+
readonly status: number;
|
|
22
|
+
/** Whether the daemon sent `Transfer-Encoding: chunked` (node:http already de-chunks it transparently either way). */
|
|
23
|
+
readonly chunked: boolean;
|
|
24
|
+
/** The `Content-Length` header value, when present and not chunked. */
|
|
25
|
+
readonly contentLength: number | undefined;
|
|
26
|
+
/** The raw, already-de-chunked response stream — `node:http` handles `Transfer-Encoding` transparently, so the frame demuxer only ever sees raw framed bytes. */
|
|
27
|
+
readonly body: IncomingMessage;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The pure seam `DockerClient.fromEnv` delegates to: given a `DOCKER_HOST`
|
|
31
|
+
* value (or `undefined`), returns the unix socket path this client should
|
|
32
|
+
* dial. A `tcp://`/`http://` `DOCKER_HOST` falls back to the default socket
|
|
33
|
+
* path rather than attempting a TCP connection — this client has no TCP
|
|
34
|
+
* transport at all (see the module docs on why: sharing an HTTP stack a
|
|
35
|
+
* consumer can bump is exactly the failure mode being avoided).
|
|
36
|
+
*/
|
|
37
|
+
export declare function socketPathFromDockerHost(host: string | undefined): string;
|
|
38
|
+
/**
|
|
39
|
+
* A from-scratch HTTP client over the Docker daemon's unix socket, built on
|
|
40
|
+
* `node:http`'s `socketPath` option rather than a general-purpose Docker SDK
|
|
41
|
+
* (`dockerode`) — the point of hand-rolling is that this client can only
|
|
42
|
+
* ever dial a unix socket path, never a TCP host. A shared HTTP stack a
|
|
43
|
+
* consuming project also depends on has, on another runtime, been observed
|
|
44
|
+
* to misroute a Docker client onto TCP `localhost:2375` instead of the
|
|
45
|
+
* daemon's real unix socket; owning this client end-to-end makes that
|
|
46
|
+
* misrouting structurally impossible; see the standing regression test in
|
|
47
|
+
* `backend.test.ts`.
|
|
48
|
+
*
|
|
49
|
+
* Every daemon endpoint this backend calls returns either a small buffered
|
|
50
|
+
* JSON body (unary calls — `request`) or a stream of raw framed bytes
|
|
51
|
+
* (`exec`/`logs` — `requestStream`). Both go through `POST
|
|
52
|
+
* /exec/{id}/start`-style ordinary responses, never `/attach`'s connection
|
|
53
|
+
* hijack, so `node:http`'s `IncomingMessage` is sufficient for either case —
|
|
54
|
+
* no need to drop to a raw socket.
|
|
55
|
+
*/
|
|
56
|
+
export declare class DockerClient {
|
|
57
|
+
private readonly socketPath;
|
|
58
|
+
private readonly connectTimeoutMs;
|
|
59
|
+
constructor(socketPath?: string, connectTimeoutMs?: number);
|
|
60
|
+
/** Builds a client honoring `DOCKER_HOST` (a `unix://` path, a bare path, or the daemon's default socket). */
|
|
61
|
+
static fromEnv(): DockerClient;
|
|
62
|
+
/** The unix socket path this client dials — never a TCP host. */
|
|
63
|
+
getSocketPath(): string;
|
|
64
|
+
/**
|
|
65
|
+
* One request whose entire response body is read into memory before
|
|
66
|
+
* returning — every daemon call this backend makes except
|
|
67
|
+
* `exec`/`logs` streaming (`requestStream`). Bounded by
|
|
68
|
+
* `RESPONSE_TIMEOUT_MS` so a wedged daemon fails this call with a named
|
|
69
|
+
* error instead of hanging the caller forever.
|
|
70
|
+
*/
|
|
71
|
+
request(method: string, path: string, body?: string): Promise<DockerResponse>;
|
|
72
|
+
/**
|
|
73
|
+
* Issues a request and returns the status plus the still-open response
|
|
74
|
+
* stream, for callers (`exec`/`logs`, the frame demuxer) that consume a
|
|
75
|
+
* de-chunked body incrementally rather than buffering it all first.
|
|
76
|
+
* Deliberately NOT wrapped in `RESPONSE_TIMEOUT_MS` — `followLogs` streams
|
|
77
|
+
* for as long as the workload runs, by design.
|
|
78
|
+
*/
|
|
79
|
+
requestStream(method: string, path: string, body?: string): Promise<DockerStreamResponse>;
|
|
80
|
+
private send;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/backend-docker/client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AA4BjD;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,QAAW,CAAC;AAEzC,4EAA4E;AAC5E,MAAM,WAAW,cAAc;IAC7B,4BAA4B;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,8BAA8B;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,+HAA+H;AAC/H,MAAM,WAAW,oBAAoB;IACnC,4BAA4B;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sHAAsH;IACtH,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,uEAAuE;IACvE,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C,iKAAiK;IACjK,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;CAChC;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAWzE;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,YAAY;IAErB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAK3B,OAAO,CAAC,QAAQ,CAAC,gBAAgB;gBALhB,UAAU,GAAE,MAA6D,EAKzE,gBAAgB,GAAE,MAA2B;IAGhE,8GAA8G;IAC9G,MAAM,CAAC,OAAO,IAAI,YAAY;IAI9B,iEAAiE;IACjE,aAAa,IAAI,MAAM;IAIvB;;;;;;OAMG;IACG,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAuBnF;;;;;;OAMG;IACG,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAU/F,OAAO,CAAC,IAAI;CAkEb"}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import * as http from "node:http";
|
|
2
|
+
import { BackendError } from "../core/errors.js";
|
|
3
|
+
const DEFAULT_SOCKET_PATH = "/var/run/docker.sock";
|
|
4
|
+
/**
|
|
5
|
+
* Ceiling on one unary request/response cycle — connect, write, read
|
|
6
|
+
* headers, read the whole body. Deliberately NOT applied to streaming reads
|
|
7
|
+
* (`followLogs`): that call is meant to block for as long as the workload
|
|
8
|
+
* runs, so bounding it here would kill a perfectly healthy long-lived
|
|
9
|
+
* stream. Unary calls (create/start/stop/exec-inspect/logs/ls) get it
|
|
10
|
+
* because a wedged daemon must fail the caller promptly instead of hanging
|
|
11
|
+
* the whole test run.
|
|
12
|
+
*/
|
|
13
|
+
const RESPONSE_TIMEOUT_MS = 600_000;
|
|
14
|
+
/**
|
|
15
|
+
* Ceiling on connecting to the daemon's unix socket and receiving response
|
|
16
|
+
* headers — the phase before `RESPONSE_TIMEOUT_MS`'s body-reading window
|
|
17
|
+
* even starts. Without this, a request whose connection or header phase
|
|
18
|
+
* never completes (an unresponsive daemon, a wedged socket) hangs `send()`
|
|
19
|
+
* forever with nothing else in this client ever getting the chance to time
|
|
20
|
+
* it out — `request()`'s own timer only starts once `send()` has already
|
|
21
|
+
* resolved with a response. Observed in practice: an intermittent hang here
|
|
22
|
+
* under Bun's `node:http` implementation, not merely a theoretical gap.
|
|
23
|
+
*/
|
|
24
|
+
const CONNECT_TIMEOUT_MS = 30_000;
|
|
25
|
+
/**
|
|
26
|
+
* Bounds how many header bytes this client will accumulate before giving up.
|
|
27
|
+
* A well-behaved daemon's status line + header block is at most a few
|
|
28
|
+
* hundred bytes; `node:http` already enforces its own such limit internally,
|
|
29
|
+
* but this constant documents the ceiling this backend would apply if it
|
|
30
|
+
* were parsing headers by hand — kept here as the single source of truth
|
|
31
|
+
* other backend code can cite.
|
|
32
|
+
*/
|
|
33
|
+
export const MAX_HEADER_BYTES = 8 * 1024;
|
|
34
|
+
/**
|
|
35
|
+
* The pure seam `DockerClient.fromEnv` delegates to: given a `DOCKER_HOST`
|
|
36
|
+
* value (or `undefined`), returns the unix socket path this client should
|
|
37
|
+
* dial. A `tcp://`/`http://` `DOCKER_HOST` falls back to the default socket
|
|
38
|
+
* path rather than attempting a TCP connection — this client has no TCP
|
|
39
|
+
* transport at all (see the module docs on why: sharing an HTTP stack a
|
|
40
|
+
* consumer can bump is exactly the failure mode being avoided).
|
|
41
|
+
*/
|
|
42
|
+
export function socketPathFromDockerHost(host) {
|
|
43
|
+
if (host === undefined) {
|
|
44
|
+
return DEFAULT_SOCKET_PATH;
|
|
45
|
+
}
|
|
46
|
+
if (host.startsWith("unix://")) {
|
|
47
|
+
return host.slice("unix://".length);
|
|
48
|
+
}
|
|
49
|
+
if (host.startsWith("/")) {
|
|
50
|
+
return host;
|
|
51
|
+
}
|
|
52
|
+
return DEFAULT_SOCKET_PATH;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A from-scratch HTTP client over the Docker daemon's unix socket, built on
|
|
56
|
+
* `node:http`'s `socketPath` option rather than a general-purpose Docker SDK
|
|
57
|
+
* (`dockerode`) — the point of hand-rolling is that this client can only
|
|
58
|
+
* ever dial a unix socket path, never a TCP host. A shared HTTP stack a
|
|
59
|
+
* consuming project also depends on has, on another runtime, been observed
|
|
60
|
+
* to misroute a Docker client onto TCP `localhost:2375` instead of the
|
|
61
|
+
* daemon's real unix socket; owning this client end-to-end makes that
|
|
62
|
+
* misrouting structurally impossible; see the standing regression test in
|
|
63
|
+
* `backend.test.ts`.
|
|
64
|
+
*
|
|
65
|
+
* Every daemon endpoint this backend calls returns either a small buffered
|
|
66
|
+
* JSON body (unary calls — `request`) or a stream of raw framed bytes
|
|
67
|
+
* (`exec`/`logs` — `requestStream`). Both go through `POST
|
|
68
|
+
* /exec/{id}/start`-style ordinary responses, never `/attach`'s connection
|
|
69
|
+
* hijack, so `node:http`'s `IncomingMessage` is sufficient for either case —
|
|
70
|
+
* no need to drop to a raw socket.
|
|
71
|
+
*/
|
|
72
|
+
export class DockerClient {
|
|
73
|
+
socketPath;
|
|
74
|
+
connectTimeoutMs;
|
|
75
|
+
constructor(socketPath = socketPathFromDockerHost(process.env["DOCKER_HOST"]),
|
|
76
|
+
// Test seam: production call sites never pass this, so every real client
|
|
77
|
+
// gets the real ceiling. A fixture that wants to prove the timeout path
|
|
78
|
+
// itself fires (without actually waiting 30s) constructs with a much
|
|
79
|
+
// smaller override instead.
|
|
80
|
+
connectTimeoutMs = CONNECT_TIMEOUT_MS) {
|
|
81
|
+
this.socketPath = socketPath;
|
|
82
|
+
this.connectTimeoutMs = connectTimeoutMs;
|
|
83
|
+
}
|
|
84
|
+
/** Builds a client honoring `DOCKER_HOST` (a `unix://` path, a bare path, or the daemon's default socket). */
|
|
85
|
+
static fromEnv() {
|
|
86
|
+
return new DockerClient(socketPathFromDockerHost(process.env["DOCKER_HOST"]));
|
|
87
|
+
}
|
|
88
|
+
/** The unix socket path this client dials — never a TCP host. */
|
|
89
|
+
getSocketPath() {
|
|
90
|
+
return this.socketPath;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* One request whose entire response body is read into memory before
|
|
94
|
+
* returning — every daemon call this backend makes except
|
|
95
|
+
* `exec`/`logs` streaming (`requestStream`). Bounded by
|
|
96
|
+
* `RESPONSE_TIMEOUT_MS` so a wedged daemon fails this call with a named
|
|
97
|
+
* error instead of hanging the caller forever.
|
|
98
|
+
*/
|
|
99
|
+
async request(method, path, body) {
|
|
100
|
+
const { res } = await this.send(method, path, body);
|
|
101
|
+
const chunks = [];
|
|
102
|
+
await new Promise((resolveBody, rejectBody) => {
|
|
103
|
+
const timer = setTimeout(() => {
|
|
104
|
+
res.destroy();
|
|
105
|
+
rejectBody(new BackendError(`${method} ${path} to the Docker daemon did not complete within ${RESPONSE_TIMEOUT_MS / 1000}s`));
|
|
106
|
+
}, RESPONSE_TIMEOUT_MS);
|
|
107
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
108
|
+
res.on("end", () => {
|
|
109
|
+
clearTimeout(timer);
|
|
110
|
+
resolveBody();
|
|
111
|
+
});
|
|
112
|
+
res.on("error", (err) => {
|
|
113
|
+
clearTimeout(timer);
|
|
114
|
+
rejectBody(new BackendError(`reading a response from the Docker daemon: ${err.message}`));
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
return { status: res.statusCode ?? 0, body: Buffer.concat(chunks) };
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Issues a request and returns the status plus the still-open response
|
|
121
|
+
* stream, for callers (`exec`/`logs`, the frame demuxer) that consume a
|
|
122
|
+
* de-chunked body incrementally rather than buffering it all first.
|
|
123
|
+
* Deliberately NOT wrapped in `RESPONSE_TIMEOUT_MS` — `followLogs` streams
|
|
124
|
+
* for as long as the workload runs, by design.
|
|
125
|
+
*/
|
|
126
|
+
async requestStream(method, path, body) {
|
|
127
|
+
const { res } = await this.send(method, path, body);
|
|
128
|
+
const transferEncoding = res.headers["transfer-encoding"];
|
|
129
|
+
const chunked = typeof transferEncoding === "string" && transferEncoding.toLowerCase().includes("chunked");
|
|
130
|
+
const contentLengthHeader = res.headers["content-length"];
|
|
131
|
+
const contentLength = typeof contentLengthHeader === "string" && !chunked ? Number.parseInt(contentLengthHeader, 10) : undefined;
|
|
132
|
+
return { status: res.statusCode ?? 0, chunked, contentLength, body: res };
|
|
133
|
+
}
|
|
134
|
+
send(method, path, body) {
|
|
135
|
+
return new Promise((resolveSend, rejectSend) => {
|
|
136
|
+
const headers = {};
|
|
137
|
+
if (body !== undefined) {
|
|
138
|
+
headers["Content-Length"] = String(Buffer.byteLength(body));
|
|
139
|
+
headers["Content-Type"] = "application/json";
|
|
140
|
+
}
|
|
141
|
+
let settled = false;
|
|
142
|
+
const req = http.request({
|
|
143
|
+
socketPath: this.socketPath,
|
|
144
|
+
method,
|
|
145
|
+
path,
|
|
146
|
+
headers,
|
|
147
|
+
// No connection pooling: `http.globalAgent`'s default keep-alive
|
|
148
|
+
// behavior holds the underlying unix-socket connection open after
|
|
149
|
+
// the response completes, hoping to reuse it for a later request
|
|
150
|
+
// to the same socket path. Nothing in this client ever reuses a
|
|
151
|
+
// connection (every call opens its own), so a pooled socket just
|
|
152
|
+
// sits idle — which keeps a bare `node --test` process (or any
|
|
153
|
+
// short-lived script) alive well past its last container
|
|
154
|
+
// operation, since Node won't exit while a socket handle is open.
|
|
155
|
+
// `agent: false` makes every request its own unshared connection
|
|
156
|
+
// that node:http tears down as soon as the response ends.
|
|
157
|
+
agent: false,
|
|
158
|
+
}, (res) => {
|
|
159
|
+
if (settled) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
settled = true;
|
|
163
|
+
clearTimeout(connectTimer);
|
|
164
|
+
resolveSend({ res });
|
|
165
|
+
});
|
|
166
|
+
const connectTimer = setTimeout(() => {
|
|
167
|
+
if (settled) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
settled = true;
|
|
171
|
+
req.destroy();
|
|
172
|
+
rejectSend(new BackendError(`${method} ${path} to the Docker daemon at ${this.socketPath} did not connect/respond within ` +
|
|
173
|
+
`${this.connectTimeoutMs / 1000}s — is the daemon running and responsive?`));
|
|
174
|
+
}, this.connectTimeoutMs);
|
|
175
|
+
req.on("error", (err) => {
|
|
176
|
+
if (settled) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
settled = true;
|
|
180
|
+
clearTimeout(connectTimer);
|
|
181
|
+
rejectSend(new BackendError(`could not connect to the Docker daemon at ${this.socketPath} — is Docker/Podman/Colima running? (${err.message})`));
|
|
182
|
+
});
|
|
183
|
+
if (body !== undefined) {
|
|
184
|
+
req.write(body);
|
|
185
|
+
}
|
|
186
|
+
req.end();
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/backend-docker/client.ts"],"names":[],"mappings":"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"}
|