openship 0.6.0 → 0.6.5
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 +7 -2
- package/dist/{bare-FSUAM5V5.js → bare-XQIPDO4R.js} +10 -9
- package/dist/{chunk-PSZSKEPA.js → chunk-3ZAGC5NT.js} +64 -17
- package/dist/{chunk-6ODUBKUB.js → chunk-5OUWIPOB.js} +69 -7
- package/dist/chunk-6HOA5JT7.js +124 -0
- package/dist/{chunk-TWJCLD4W.js → chunk-6JELW75A.js} +4 -4
- package/dist/{chunk-GUQ3QCJ7.js → chunk-7334NSJL.js} +399 -369
- package/dist/chunk-7S4USQYF.js +1753 -0
- package/dist/{chunk-3LDWWY7U.js → chunk-A7EWM2KA.js} +41 -7
- package/dist/chunk-ELGRMZOW.js +25 -0
- package/dist/chunk-EZPPTWJQ.js +292 -0
- package/dist/{chunk-IXM5245F.js → chunk-GRTXMUHA.js} +553 -538
- package/dist/chunk-HPXP6LXZ.js +428 -0
- package/dist/{chunk-OTLSJYAC.js → chunk-LEYSL46B.js} +2135 -85
- package/dist/{chunk-EMXI4UCU.js → chunk-LFCSC4IU.js} +29 -10
- package/dist/{chunk-JIMUYEKN.js → chunk-MSZUCNPV.js} +1 -1
- package/dist/{chunk-OGY63DGD.js → chunk-PZRRL5PC.js} +71 -28
- package/dist/{chunk-VE2YOK4I.js → chunk-QLX7NBEA.js} +1218 -588
- package/dist/chunk-RFWJE7ZN.js +68 -0
- package/dist/{chunk-VDDMADOF.js → chunk-TM2IQPYU.js} +16 -2
- package/dist/{chunk-3FYIXVX5.js → chunk-VDESJSG6.js} +451 -106
- package/dist/{chunk-B3CE2OMR.js → chunk-VHMOQL5Y.js} +276 -50
- package/dist/{chunk-QEH4QUBX.js → chunk-VLQ3UIVO.js} +393 -53
- package/dist/{chunk-VZ2ZHPS2.js → chunk-WE7BXW6L.js} +3 -9
- package/dist/chunk-WFRXYWK2.js +444 -0
- package/dist/chunk-XRNRT37W.js +316 -0
- package/dist/{chunk-EP2TNALB.js → chunk-XVOBL7BC.js} +31 -8
- package/dist/cloud-3DODOWTR.js +36 -0
- package/dist/{detect-ALF4GXRX.js → detect-P4CIQBRH.js} +3 -3
- package/dist/{docker-QECQJ5P4.js → docker-3K2NEBD6.js} +15 -8
- package/dist/{docker-edge-executor-62N64MGS.js → docker-edge-executor-YZZ6P7IC.js} +5 -5
- package/dist/{edge-import-CQWHN2Q3.js → edge-import-MCJNVTZ2.js} +26 -23
- package/dist/edge-preflight-ES6FH4JK.js +42 -0
- package/dist/{ensure-container-edge-MJVXJQG5.js → ensure-container-edge-YPITNH45.js} +9 -8
- package/dist/{executor-JELARVKC.js → executor-DZGX55K2.js} +12 -5
- package/dist/host-channel-preflight-MUXE3KZU.js +49 -0
- package/dist/index.js +3668 -4063
- package/dist/{nginx-AXGNWRKZ.js → nginx-NSAHWGJV.js} +11 -5
- package/dist/node-bootstrap.js +241 -0
- package/dist/node-entry.js +52 -1
- package/dist/{openresty-lua-IE4P5YQX.js → openresty-lua-DR4D3EZ6.js} +6 -2
- package/dist/privilege-YTVI4SKF.js +19 -0
- package/dist/server/engine/samples/fail2ban/jail.d/sshd.local +4 -1
- package/dist/server/index.js +21501 -13137
- package/dist/server/lua/geo_country.lua +4 -3
- package/dist/server/lua/pipe_stream.lua +52 -39
- package/dist/server/lua/site_logger.lua +22 -24
- package/dist/server/migrations/0098_instance_product_mode.sql +12 -0
- package/dist/server/migrations/0099_mail_webmail_project.sql +24 -0
- package/dist/server/migrations/0100_server_ssh_private_key.sql +11 -0
- package/dist/server/migrations/0101_mail_resume_step.sql +12 -0
- package/dist/server/migrations/0102_project_workload_axes.sql +19 -0
- package/dist/server/migrations/0103_dns_credential.sql +41 -0
- package/dist/server/migrations/0104_instance_host_control.sql +15 -0
- package/dist/server/migrations/meta/_journal.json +49 -0
- package/dist/{setup-FA4PW4NU.js → setup-WUUZURHS.js} +10 -9
- package/dist/up-XEO3SR6Z.js +58 -0
- package/package.json +4 -2
- package/dist/chunk-IW2UKBVF.js +0 -144
- package/dist/chunk-ZCFVHA25.js +0 -89
- package/dist/cloud-E5M6VMCG.js +0 -31
- package/dist/server/lua/pipe_log.lua +0 -77
- /package/dist/{dist-es-JGBPABJI.js → dist-es-ZP5QDSJ3.js} +0 -0
|
@@ -9,33 +9,41 @@ import {
|
|
|
9
9
|
resolveDockerfileCandidates,
|
|
10
10
|
resolveServiceDockerfile,
|
|
11
11
|
staticBuilderOutputPath
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-LFCSC4IU.js";
|
|
13
13
|
import {
|
|
14
|
+
BuildCancelledError,
|
|
14
15
|
BuildLogger,
|
|
15
16
|
assembleGitClone,
|
|
17
|
+
killProcessesUnderDir,
|
|
16
18
|
materializeGitSsh,
|
|
17
19
|
parseLogLevel,
|
|
18
20
|
shellGitSshWriter,
|
|
19
21
|
sq,
|
|
20
22
|
transferLocalDirectory
|
|
21
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-TM2IQPYU.js";
|
|
22
24
|
import {
|
|
25
|
+
DOCKER_DIAL_STDIO_COMMAND,
|
|
26
|
+
attachDialStdioDiagnostics,
|
|
23
27
|
connectSshClient,
|
|
28
|
+
dialStdioDiagnostics,
|
|
24
29
|
execSshCommand,
|
|
25
|
-
|
|
30
|
+
openDockerDialStdioChannel,
|
|
26
31
|
openSshUnixSocket
|
|
27
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-5OUWIPOB.js";
|
|
28
33
|
import {
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
dirOf,
|
|
35
|
+
ensureOwnedDir
|
|
36
|
+
} from "./chunk-WFRXYWK2.js";
|
|
31
37
|
import {
|
|
32
38
|
formatCpuCores,
|
|
33
39
|
formatMemoryMb,
|
|
34
40
|
hasCpuLimit,
|
|
35
41
|
hasMemoryLimit,
|
|
42
|
+
isRuntimeNotFoundError,
|
|
43
|
+
ownsNetworkEndpoint,
|
|
36
44
|
safeErrorMessage,
|
|
37
45
|
withTimeout
|
|
38
|
-
} from "./chunk-
|
|
46
|
+
} from "./chunk-LEYSL46B.js";
|
|
39
47
|
import {
|
|
40
48
|
__commonJS,
|
|
41
49
|
__require,
|
|
@@ -4842,9 +4850,9 @@ import { join } from "path";
|
|
|
4842
4850
|
import net2 from "net";
|
|
4843
4851
|
var DEFAULT_REMOTE_DOCKER_SOCKET_PATH = "/var/run/docker.sock";
|
|
4844
4852
|
var resolvedDockerSocketPathCache = /* @__PURE__ */ new WeakMap();
|
|
4845
|
-
var DOCKER_DIAL_STDIO_COMMAND = "docker system dial-stdio";
|
|
4846
4853
|
var STREAMLOCAL_PROBE_TIMEOUT_MS = 8e3;
|
|
4847
4854
|
var STREAMLOCAL_DATA_VERIFY_TIMEOUT_MS = 3e3;
|
|
4855
|
+
var DIAL_STDIO_ANSWER_TIMEOUT_MS = 3e3;
|
|
4848
4856
|
var VERIFY_BUFFER_CAP_BYTES = 8 * 1024 * 1024;
|
|
4849
4857
|
function toSshConfig(opts) {
|
|
4850
4858
|
return {
|
|
@@ -4895,12 +4903,8 @@ async function discoverRemoteDockerSocketPathsWithClient(client) {
|
|
|
4895
4903
|
return normalizeSocketPathLines(lines);
|
|
4896
4904
|
}
|
|
4897
4905
|
async function discoverRemoteDockerSocketPathsWithExecutor(executor) {
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
return normalizeSocketPathLines(output.split(/\r?\n/));
|
|
4901
|
-
} catch {
|
|
4902
|
-
return [];
|
|
4903
|
-
}
|
|
4906
|
+
const output = await executor.exec(DOCKER_SOCKET_DISCOVERY_SCRIPT, { timeout: 1e4 });
|
|
4907
|
+
return normalizeSocketPathLines(output.split(/\r?\n/));
|
|
4904
4908
|
}
|
|
4905
4909
|
async function discoverRemoteDockerSocketPaths(opts) {
|
|
4906
4910
|
if (opts.executor) {
|
|
@@ -4923,7 +4927,13 @@ async function resolveRemoteDockerSocketPath(opts) {
|
|
|
4923
4927
|
if (cachedPath) {
|
|
4924
4928
|
return cachedPath;
|
|
4925
4929
|
}
|
|
4926
|
-
const pendingPath = discoverRemoteDockerSocketPaths(opts).then(
|
|
4930
|
+
const pendingPath = discoverRemoteDockerSocketPaths(opts).then(
|
|
4931
|
+
(paths) => paths[0] ?? DEFAULT_REMOTE_DOCKER_SOCKET_PATH,
|
|
4932
|
+
() => {
|
|
4933
|
+
resolvedDockerSocketPathCache.delete(opts);
|
|
4934
|
+
return DEFAULT_REMOTE_DOCKER_SOCKET_PATH;
|
|
4935
|
+
}
|
|
4936
|
+
);
|
|
4927
4937
|
resolvedDockerSocketPathCache.set(opts, pendingPath);
|
|
4928
4938
|
return pendingPath;
|
|
4929
4939
|
}
|
|
@@ -4943,35 +4953,98 @@ async function openStreamlocalUpstream(opts) {
|
|
|
4943
4953
|
throw error;
|
|
4944
4954
|
}
|
|
4945
4955
|
}
|
|
4946
|
-
function
|
|
4956
|
+
function respondBadGateway(client, reason) {
|
|
4957
|
+
if (client.destroyed || client.writableEnded) {
|
|
4958
|
+
client.destroy();
|
|
4959
|
+
return;
|
|
4960
|
+
}
|
|
4961
|
+
const body = JSON.stringify({ message: reason });
|
|
4962
|
+
try {
|
|
4963
|
+
client.end(
|
|
4964
|
+
`HTTP/1.1 502 Bad Gateway\r
|
|
4965
|
+
Content-Type: application/json\r
|
|
4966
|
+
Content-Length: ${Buffer.byteLength(body)}\r
|
|
4967
|
+
Connection: close\r
|
|
4968
|
+
\r
|
|
4969
|
+
` + body
|
|
4970
|
+
);
|
|
4971
|
+
} catch {
|
|
4972
|
+
client.destroy();
|
|
4973
|
+
}
|
|
4974
|
+
}
|
|
4975
|
+
function pipeThrough(client, upstream, handoff) {
|
|
4976
|
+
let answered = handoff.answered;
|
|
4947
4977
|
const teardown = () => {
|
|
4948
4978
|
client.destroy();
|
|
4949
4979
|
upstream.destroy();
|
|
4950
4980
|
};
|
|
4951
4981
|
client.on("error", teardown);
|
|
4952
|
-
upstream.on("error",
|
|
4982
|
+
upstream.on("error", () => {
|
|
4983
|
+
if (!answered) respondBadGateway(client, handoff.unansweredReason());
|
|
4984
|
+
upstream.destroy();
|
|
4985
|
+
});
|
|
4953
4986
|
client.once("close", () => upstream.destroy());
|
|
4954
|
-
upstream.once("close", () =>
|
|
4987
|
+
upstream.once("close", () => {
|
|
4988
|
+
if (!answered) {
|
|
4989
|
+
respondBadGateway(client, handoff.unansweredReason());
|
|
4990
|
+
return;
|
|
4991
|
+
}
|
|
4992
|
+
if (!client.destroyed) client.end();
|
|
4993
|
+
});
|
|
4994
|
+
upstream.on("data", () => {
|
|
4995
|
+
answered = true;
|
|
4996
|
+
});
|
|
4955
4997
|
client.pipe(upstream);
|
|
4956
|
-
upstream.pipe(client);
|
|
4998
|
+
upstream.pipe(client, { end: false });
|
|
4999
|
+
upstream.once("end", () => {
|
|
5000
|
+
if (answered && !client.writableEnded) client.end();
|
|
5001
|
+
});
|
|
4957
5002
|
}
|
|
4958
5003
|
function awaitFirstByte(upstream, ms) {
|
|
4959
5004
|
return new Promise((resolve) => {
|
|
4960
|
-
const finish = (
|
|
5005
|
+
const finish = (outcome) => {
|
|
4961
5006
|
clearTimeout(timer);
|
|
4962
5007
|
upstream.removeListener("data", onData);
|
|
4963
5008
|
upstream.removeListener("error", onFail);
|
|
4964
5009
|
upstream.removeListener("close", onFail);
|
|
4965
|
-
resolve(
|
|
5010
|
+
resolve(outcome);
|
|
4966
5011
|
};
|
|
4967
|
-
const onData = (chunk) => finish(chunk);
|
|
4968
|
-
const onFail = () => finish(
|
|
4969
|
-
const timer = setTimeout(() => finish(
|
|
5012
|
+
const onData = (chunk) => finish({ kind: "data", chunk });
|
|
5013
|
+
const onFail = () => finish({ kind: "closed" });
|
|
5014
|
+
const timer = setTimeout(() => finish({ kind: "silent" }), ms);
|
|
4970
5015
|
upstream.on("data", onData);
|
|
4971
5016
|
upstream.once("error", onFail);
|
|
4972
5017
|
upstream.once("close", onFail);
|
|
4973
5018
|
});
|
|
4974
5019
|
}
|
|
5020
|
+
function looksLikeHttpResponse(chunk) {
|
|
5021
|
+
const marker = "HTTP/";
|
|
5022
|
+
const head = chunk.subarray(0, marker.length).toString("latin1");
|
|
5023
|
+
return marker.startsWith(head) || head.startsWith(marker);
|
|
5024
|
+
}
|
|
5025
|
+
var BRIDGE_FAILURE_HINT = "Check that Docker is installed and running on the server (`docker info`) and that the SSH user can reach its socket.";
|
|
5026
|
+
function sentence(text) {
|
|
5027
|
+
const trimmed = text.trim();
|
|
5028
|
+
return /[.!?]$/.test(trimmed) ? trimmed : `${trimmed}.`;
|
|
5029
|
+
}
|
|
5030
|
+
function bridgeFailureReason(opts, cause) {
|
|
5031
|
+
const host = opts.host?.trim() || "the remote host";
|
|
5032
|
+
return `Could not reach the Docker daemon on ${host} over SSH: ${sentence(cause)} ${BRIDGE_FAILURE_HINT}`;
|
|
5033
|
+
}
|
|
5034
|
+
function upstreamClosedReason(opts, upstream, transport) {
|
|
5035
|
+
return bridgeFailureReason(
|
|
5036
|
+
opts,
|
|
5037
|
+
dialStdioDiagnostics(upstream) || `the ${transport} channel closed before answering the request`
|
|
5038
|
+
);
|
|
5039
|
+
}
|
|
5040
|
+
var REPLAY_SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
5041
|
+
function requestIsReplaySafe(chunks) {
|
|
5042
|
+
const head = chunks.find((c) => c.length > 0);
|
|
5043
|
+
if (!head) return true;
|
|
5044
|
+
const space = head.indexOf(32);
|
|
5045
|
+
if (space <= 0) return false;
|
|
5046
|
+
return REPLAY_SAFE_METHODS.has(head.subarray(0, space).toString("latin1"));
|
|
5047
|
+
}
|
|
4975
5048
|
function captureClient(client) {
|
|
4976
5049
|
const chunks = [];
|
|
4977
5050
|
let bytes = 0;
|
|
@@ -5006,9 +5079,15 @@ function captureClient(client) {
|
|
|
5006
5079
|
get overflowed() {
|
|
5007
5080
|
return overflowed;
|
|
5008
5081
|
},
|
|
5082
|
+
/** Whether this request may be handed to a second upstream — see
|
|
5083
|
+
* `requestIsReplaySafe`. Read at the moment of the decision, not at capture time. */
|
|
5084
|
+
get replaySafe() {
|
|
5085
|
+
return requestIsReplaySafe(chunks);
|
|
5086
|
+
},
|
|
5009
5087
|
/** Replay everything buffered so far onto `upstream` and forward the rest
|
|
5010
|
-
* live.
|
|
5011
|
-
* buffer is
|
|
5088
|
+
* live. Calling it again on a different upstream (the fallback) replays the
|
|
5089
|
+
* whole buffer — complete, because nothing is dropped until `commit`, but a
|
|
5090
|
+
* second dispatch to the daemon: gate that on `replaySafe`. */
|
|
5012
5091
|
feed(upstream) {
|
|
5013
5092
|
for (const chunk of chunks) upstream.write(chunk);
|
|
5014
5093
|
live = upstream;
|
|
@@ -5019,25 +5098,30 @@ function captureClient(client) {
|
|
|
5019
5098
|
live = null;
|
|
5020
5099
|
},
|
|
5021
5100
|
/** Terminal: detach and hand the socket to the (already-fed) `upstream` via a
|
|
5022
|
-
* bidi pipe, or destroy it if the client already went away.
|
|
5023
|
-
|
|
5101
|
+
* bidi pipe, or destroy it if the client already went away. `handoff` tells the
|
|
5102
|
+
* pipe whether a response byte has already been forwarded, and what to say if
|
|
5103
|
+
* the upstream closes without ever producing one. */
|
|
5104
|
+
commit(upstream, handoff) {
|
|
5024
5105
|
detach();
|
|
5025
5106
|
chunks.length = 0;
|
|
5026
5107
|
if (closed) {
|
|
5027
5108
|
upstream.destroy();
|
|
5028
5109
|
return;
|
|
5029
5110
|
}
|
|
5030
|
-
pipeThrough(client, upstream);
|
|
5111
|
+
pipeThrough(client, upstream, handoff);
|
|
5031
5112
|
},
|
|
5032
|
-
/** Terminal: no usable upstream.
|
|
5033
|
-
*
|
|
5034
|
-
*
|
|
5035
|
-
*
|
|
5036
|
-
*
|
|
5037
|
-
*
|
|
5038
|
-
|
|
5113
|
+
/** Terminal: no usable upstream. Answer the request with `reason` (a real HTTP
|
|
5114
|
+
* 502 — see `respondBadGateway`) instead of destroying the socket, which is
|
|
5115
|
+
* what turned every one of these into `socket hang up`.
|
|
5116
|
+
*
|
|
5117
|
+
* Written BEFORE detach so the socket still has an error listener during the
|
|
5118
|
+
* write, and never with `destroy(err)`: a server-side socket cannot transmit a
|
|
5119
|
+
* JS Error to its peer, and destroy(err) emits a local 'error' event that —
|
|
5120
|
+
* once detach() has removed this socket's listener — would be unhandled and
|
|
5121
|
+
* take down the process over one dead Docker request. */
|
|
5122
|
+
fail(reason) {
|
|
5123
|
+
respondBadGateway(client, reason);
|
|
5039
5124
|
detach();
|
|
5040
|
-
client.destroy();
|
|
5041
5125
|
}
|
|
5042
5126
|
};
|
|
5043
5127
|
}
|
|
@@ -5049,12 +5133,7 @@ function createDockerSshBridge(opts) {
|
|
|
5049
5133
|
let pooledUnreliable = false;
|
|
5050
5134
|
const openDialStdioUpstream = async (forceEphemeral = false) => {
|
|
5051
5135
|
if (!forceEphemeral && opts.executor?.openDockerDialStdio) {
|
|
5052
|
-
|
|
5053
|
-
stream.once(
|
|
5054
|
-
"error",
|
|
5055
|
-
(e) => console.warn(`[docker-ssh] dial-stdio channel error: ${safeErrorMessage(e)}`)
|
|
5056
|
-
);
|
|
5057
|
-
return stream;
|
|
5136
|
+
return attachDialStdioDiagnostics(await opts.executor.openDockerDialStdio());
|
|
5058
5137
|
}
|
|
5059
5138
|
if (!dialClient) {
|
|
5060
5139
|
const client = await connectSshClient(toSshConfig(opts));
|
|
@@ -5063,7 +5142,34 @@ function createDockerSshBridge(opts) {
|
|
|
5063
5142
|
});
|
|
5064
5143
|
dialClient = client;
|
|
5065
5144
|
}
|
|
5066
|
-
return
|
|
5145
|
+
return openDockerDialStdioChannel(dialClient);
|
|
5146
|
+
};
|
|
5147
|
+
const serveDialStdio = async (capture, client, upstream) => {
|
|
5148
|
+
capture.feed(upstream);
|
|
5149
|
+
const first = capture.overflowed ? { kind: "silent" } : await awaitFirstByte(upstream, DIAL_STDIO_ANSWER_TIMEOUT_MS);
|
|
5150
|
+
if (first.kind === "closed") {
|
|
5151
|
+
capture.stop();
|
|
5152
|
+
upstream.destroy();
|
|
5153
|
+
capture.fail(upstreamClosedReason(opts, upstream, DOCKER_DIAL_STDIO_COMMAND));
|
|
5154
|
+
return;
|
|
5155
|
+
}
|
|
5156
|
+
if (first.kind === "data" && !looksLikeHttpResponse(first.chunk)) {
|
|
5157
|
+
capture.stop();
|
|
5158
|
+
upstream.destroy();
|
|
5159
|
+
const banner = first.chunk.toString("utf8").trim().replace(/\s+/g, " ").slice(0, 300);
|
|
5160
|
+
capture.fail(
|
|
5161
|
+
bridgeFailureReason(
|
|
5162
|
+
opts,
|
|
5163
|
+
`the SSH session answered ${JSON.stringify(banner)} instead of a Docker API response \u2014 something on the remote account (an sshd forced command, or a login banner) is intercepting \`${DOCKER_DIAL_STDIO_COMMAND}\``
|
|
5164
|
+
)
|
|
5165
|
+
);
|
|
5166
|
+
return;
|
|
5167
|
+
}
|
|
5168
|
+
if (first.kind === "data") client.write(first.chunk);
|
|
5169
|
+
capture.commit(upstream, {
|
|
5170
|
+
answered: first.kind === "data",
|
|
5171
|
+
unansweredReason: () => upstreamClosedReason(opts, upstream, DOCKER_DIAL_STDIO_COMMAND)
|
|
5172
|
+
});
|
|
5067
5173
|
};
|
|
5068
5174
|
const decideMode = async () => {
|
|
5069
5175
|
let probe = null;
|
|
@@ -5111,24 +5217,20 @@ function createDockerSshBridge(opts) {
|
|
|
5111
5217
|
modeDecision ??= decideMode().then((mode) => upstreamMode = mode);
|
|
5112
5218
|
return modeDecision;
|
|
5113
5219
|
};
|
|
5114
|
-
const downgradeToDialStdio = async (capture, reason) => {
|
|
5220
|
+
const downgradeToDialStdio = async (capture, client, reason) => {
|
|
5115
5221
|
console.warn(
|
|
5116
5222
|
`[docker-ssh] streamlocal unusable (${opts.host ?? "?"}): ${reason} \u2014 downgrading this bridge to dial-stdio (fresh connection) and replaying the buffered request.`
|
|
5117
5223
|
);
|
|
5118
5224
|
upstreamMode = "dialstdio";
|
|
5119
5225
|
pooledUnreliable = true;
|
|
5120
|
-
|
|
5121
|
-
capture.feed(upstream);
|
|
5122
|
-
capture.commit(upstream);
|
|
5226
|
+
await serveDialStdio(capture, client, await openDialStdioUpstream(true));
|
|
5123
5227
|
};
|
|
5124
5228
|
const bridgeClient = async (client) => {
|
|
5125
5229
|
const capture = captureClient(client);
|
|
5126
5230
|
try {
|
|
5127
5231
|
const mode = await ensureMode();
|
|
5128
5232
|
if (mode === "dialstdio") {
|
|
5129
|
-
|
|
5130
|
-
capture.feed(upstream);
|
|
5131
|
-
capture.commit(upstream);
|
|
5233
|
+
await serveDialStdio(capture, client, await openDialStdioUpstream(pooledUnreliable));
|
|
5132
5234
|
return;
|
|
5133
5235
|
}
|
|
5134
5236
|
let channel;
|
|
@@ -5139,22 +5241,36 @@ function createDockerSshBridge(opts) {
|
|
|
5139
5241
|
`channel open timed out after ${STREAMLOCAL_DATA_VERIFY_TIMEOUT_MS / 1e3}s`
|
|
5140
5242
|
);
|
|
5141
5243
|
} catch (err) {
|
|
5142
|
-
await downgradeToDialStdio(capture, safeErrorMessage(err));
|
|
5244
|
+
await downgradeToDialStdio(capture, client, safeErrorMessage(err));
|
|
5143
5245
|
return;
|
|
5144
5246
|
}
|
|
5145
5247
|
capture.feed(channel);
|
|
5146
|
-
const firstByte = capture.overflowed ?
|
|
5147
|
-
if (capture.overflowed || firstByte) {
|
|
5148
|
-
if (firstByte) client.write(firstByte);
|
|
5149
|
-
capture.commit(channel
|
|
5248
|
+
const firstByte = capture.overflowed ? { kind: "silent" } : await awaitFirstByte(channel, STREAMLOCAL_DATA_VERIFY_TIMEOUT_MS);
|
|
5249
|
+
if (capture.overflowed || firstByte.kind === "data") {
|
|
5250
|
+
if (firstByte.kind === "data") client.write(firstByte.chunk);
|
|
5251
|
+
capture.commit(channel, {
|
|
5252
|
+
answered: firstByte.kind === "data",
|
|
5253
|
+
unansweredReason: () => upstreamClosedReason(opts, channel, "streamlocal")
|
|
5254
|
+
});
|
|
5255
|
+
return;
|
|
5256
|
+
}
|
|
5257
|
+
if (firstByte.kind === "silent" && !capture.replaySafe) {
|
|
5258
|
+
capture.commit(channel, {
|
|
5259
|
+
answered: false,
|
|
5260
|
+
unansweredReason: () => upstreamClosedReason(opts, channel, "streamlocal")
|
|
5261
|
+
});
|
|
5150
5262
|
return;
|
|
5151
5263
|
}
|
|
5152
5264
|
capture.stop();
|
|
5153
5265
|
channel.destroy();
|
|
5154
|
-
await downgradeToDialStdio(
|
|
5266
|
+
await downgradeToDialStdio(
|
|
5267
|
+
capture,
|
|
5268
|
+
client,
|
|
5269
|
+
firstByte.kind === "closed" ? "channel closed before answering" : "channel opened but no data flowed"
|
|
5270
|
+
);
|
|
5155
5271
|
} catch (err) {
|
|
5156
5272
|
console.warn(`[docker-ssh] bridge client failed (${opts.host ?? "?"}): ${safeErrorMessage(err)}`);
|
|
5157
|
-
capture.fail();
|
|
5273
|
+
capture.fail(bridgeFailureReason(opts, safeErrorMessage(err)));
|
|
5158
5274
|
}
|
|
5159
5275
|
};
|
|
5160
5276
|
const server = net2.createServer((client) => {
|
|
@@ -5330,6 +5446,35 @@ var RESTART_POLICIES = {
|
|
|
5330
5446
|
no: { Name: "no", MaximumRetryCount: 0 }
|
|
5331
5447
|
};
|
|
5332
5448
|
var DOCKER_BUILD_IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
5449
|
+
var activeDockerBuilds = /* @__PURE__ */ new Map();
|
|
5450
|
+
var pendingDockerBuildCancellations = /* @__PURE__ */ new Map();
|
|
5451
|
+
var PENDING_CANCEL_TTL_MS = 10 * 60 * 1e3;
|
|
5452
|
+
function cancelCovers(cancelledId, sessionId) {
|
|
5453
|
+
return sessionId === cancelledId || sessionId.startsWith(`${cancelledId}-`);
|
|
5454
|
+
}
|
|
5455
|
+
function recordPendingCancellation(sessionId) {
|
|
5456
|
+
clearTimeout(pendingDockerBuildCancellations.get(sessionId));
|
|
5457
|
+
const timer = setTimeout(
|
|
5458
|
+
() => pendingDockerBuildCancellations.delete(sessionId),
|
|
5459
|
+
PENDING_CANCEL_TTL_MS
|
|
5460
|
+
);
|
|
5461
|
+
timer.unref?.();
|
|
5462
|
+
pendingDockerBuildCancellations.set(sessionId, timer);
|
|
5463
|
+
}
|
|
5464
|
+
function registerDockerBuild(sessionId) {
|
|
5465
|
+
const abort = new AbortController();
|
|
5466
|
+
for (const cancelledId of pendingDockerBuildCancellations.keys()) {
|
|
5467
|
+
if (cancelCovers(cancelledId, sessionId)) {
|
|
5468
|
+
abort.abort();
|
|
5469
|
+
break;
|
|
5470
|
+
}
|
|
5471
|
+
}
|
|
5472
|
+
activeDockerBuilds.set(sessionId, abort);
|
|
5473
|
+
return abort;
|
|
5474
|
+
}
|
|
5475
|
+
function releaseDockerBuild(sessionId, abort) {
|
|
5476
|
+
if (activeDockerBuilds.get(sessionId) === abort) activeDockerBuilds.delete(sessionId);
|
|
5477
|
+
}
|
|
5333
5478
|
var DEFAULT_IN_CONTAINER_EXEC_TIMEOUT_MS = 12e4;
|
|
5334
5479
|
var SIGTERM_EXIT_CODE = 143;
|
|
5335
5480
|
var IN_CONTAINER_EXEC_WATCHDOG = [
|
|
@@ -5475,7 +5620,7 @@ function parsePortBindings(portSpecs) {
|
|
|
5475
5620
|
}
|
|
5476
5621
|
return { exposedPorts, portBindings };
|
|
5477
5622
|
}
|
|
5478
|
-
function packBuildContext(contextDir, entries) {
|
|
5623
|
+
function packBuildContext(contextDir, entries, cancelSignal) {
|
|
5479
5624
|
const controller = new AbortController();
|
|
5480
5625
|
const pack2 = tarFs.pack(contextDir, { entries });
|
|
5481
5626
|
const body = pack2.pipe(createGzip());
|
|
@@ -5484,6 +5629,10 @@ function packBuildContext(contextDir, entries) {
|
|
|
5484
5629
|
contextError ??= err instanceof Error ? err : new Error(String(err));
|
|
5485
5630
|
if (!controller.signal.aborted) controller.abort();
|
|
5486
5631
|
};
|
|
5632
|
+
if (cancelSignal) {
|
|
5633
|
+
if (cancelSignal.aborted) controller.abort();
|
|
5634
|
+
else cancelSignal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
5635
|
+
}
|
|
5487
5636
|
pack2.once("error", capture);
|
|
5488
5637
|
body.once("error", capture);
|
|
5489
5638
|
return { body, abortSignal: controller.signal, takeError: () => contextError };
|
|
@@ -5519,7 +5668,9 @@ function toDockerHealthcheck(hc) {
|
|
|
5519
5668
|
if (typeof hc.test === "string" && hc.test.trim()) {
|
|
5520
5669
|
Test = ["CMD-SHELL", hc.test];
|
|
5521
5670
|
} else if (Array.isArray(hc.test) && hc.test.length > 0) {
|
|
5522
|
-
|
|
5671
|
+
const head = hc.test[0];
|
|
5672
|
+
if (head === "NONE") return { Test: ["NONE"] };
|
|
5673
|
+
Test = head === "CMD" || head === "CMD-SHELL" ? [...hc.test] : ["CMD", ...hc.test];
|
|
5523
5674
|
}
|
|
5524
5675
|
if (!Test) return void 0;
|
|
5525
5676
|
const Interval = parseDurationNs(hc.interval);
|
|
@@ -5533,6 +5684,28 @@ function toDockerHealthcheck(hc) {
|
|
|
5533
5684
|
...StartPeriod !== void 0 && { StartPeriod }
|
|
5534
5685
|
};
|
|
5535
5686
|
}
|
|
5687
|
+
function toStopConfig(advanced) {
|
|
5688
|
+
const out = {};
|
|
5689
|
+
const signal = advanced?.stopSignal?.trim();
|
|
5690
|
+
if (signal) out.StopSignal = signal;
|
|
5691
|
+
const graceNs = parseDurationNs(advanced?.stopGracePeriod);
|
|
5692
|
+
if (graceNs !== void 0) {
|
|
5693
|
+
out.StopTimeout = graceNs <= 0 ? 0 : Math.max(1, Math.round(graceNs / 1e9));
|
|
5694
|
+
}
|
|
5695
|
+
return out;
|
|
5696
|
+
}
|
|
5697
|
+
async function gracefulStopForGrace(container) {
|
|
5698
|
+
let stopTimeout;
|
|
5699
|
+
try {
|
|
5700
|
+
stopTimeout = (await container.inspect()).Config.StopTimeout;
|
|
5701
|
+
} catch {
|
|
5702
|
+
return;
|
|
5703
|
+
}
|
|
5704
|
+
if (typeof stopTimeout === "number" && stopTimeout > 0) {
|
|
5705
|
+
await container.stop().catch(() => {
|
|
5706
|
+
});
|
|
5707
|
+
}
|
|
5708
|
+
}
|
|
5536
5709
|
function hasDockerFrameHeader(buf, offset = 0) {
|
|
5537
5710
|
return buf.length >= offset + 8 && (buf[offset] === 1 || buf[offset] === 2) && buf[offset + 1] === 0 && buf[offset + 2] === 0 && buf[offset + 3] === 0;
|
|
5538
5711
|
}
|
|
@@ -5654,7 +5827,15 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
5654
5827
|
"deploymentContainerQuery",
|
|
5655
5828
|
"hostContainerQuery",
|
|
5656
5829
|
"stabilityProbe",
|
|
5657
|
-
"containerEvents"
|
|
5830
|
+
"containerEvents",
|
|
5831
|
+
// Docker implements `inContainerExecutor` but had never DECLARED the capability.
|
|
5832
|
+
// Nothing gated on it (every caller tested for the method), so the omission was
|
|
5833
|
+
// invisible — but it meant the honest answer to supports("inContainerExec") was
|
|
5834
|
+
// wrong for the one runtime that best supports it.
|
|
5835
|
+
"inContainerExec",
|
|
5836
|
+
// A docker exec lands in the container's own namespaces, so a command run
|
|
5837
|
+
// through it cannot reach the host. Bare deliberately does NOT declare this.
|
|
5838
|
+
"isolatedExec"
|
|
5658
5839
|
]);
|
|
5659
5840
|
/** Docker honors every extended compose key we currently support. */
|
|
5660
5841
|
unsupportedComposeKeys = /* @__PURE__ */ new Set();
|
|
@@ -5901,7 +6082,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
5901
6082
|
* context dir. One image; `dockerfileName` selects which Dockerfile in the
|
|
5902
6083
|
* shared tree to use (so N services can build from one transferred context).
|
|
5903
6084
|
*/
|
|
5904
|
-
async buildImageOnRemote(config, remoteContextDir, dockerfileName, tag, log) {
|
|
6085
|
+
async buildImageOnRemote(config, remoteContextDir, dockerfileName, tag, log, signal) {
|
|
5905
6086
|
const executor = this.connectionOptions?.executor;
|
|
5906
6087
|
if (!executor) throw new Error("SSH build path requires an executor on connectionOptions");
|
|
5907
6088
|
const buildArgs = Object.entries({
|
|
@@ -5916,10 +6097,15 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
5916
6097
|
log.log(`Running on remote: ${buildCmd}`);
|
|
5917
6098
|
log.log("\u2500\u2500\u2500 docker build output \u2500\u2500\u2500");
|
|
5918
6099
|
this.emitDockerStep(log, "install", "running", "Running install inside container (docker build)");
|
|
5919
|
-
const { code } = await executor.streamExec(
|
|
5920
|
-
|
|
5921
|
-
|
|
6100
|
+
const { code } = await executor.streamExec(
|
|
6101
|
+
buildCmd,
|
|
6102
|
+
(entry) => {
|
|
6103
|
+
log.log(entry.message, parseLogLevel(entry.message));
|
|
6104
|
+
},
|
|
6105
|
+
{ signal }
|
|
6106
|
+
);
|
|
5922
6107
|
log.log("\u2500\u2500\u2500 end docker build output \u2500\u2500\u2500");
|
|
6108
|
+
if (signal?.aborted) throw new BuildCancelledError();
|
|
5923
6109
|
if (code !== 0) throw new Error(`docker build exited with code ${code}`);
|
|
5924
6110
|
this.emitDockerStep(log, "install", "completed", "Image build finished");
|
|
5925
6111
|
}
|
|
@@ -6038,11 +6224,18 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6038
6224
|
await executor.writeFile(`${remoteContextDir}/${generatedName}`, generateDockerfile(config));
|
|
6039
6225
|
return generatedName;
|
|
6040
6226
|
}
|
|
6041
|
-
async buildViaSshTarPipe(config, buildContext, tag, log) {
|
|
6227
|
+
async buildViaSshTarPipe(config, buildContext, tag, log, signal) {
|
|
6042
6228
|
const remoteContextDir = `/tmp/openship-build-${config.sessionId}`;
|
|
6043
6229
|
try {
|
|
6044
6230
|
await this.transferBuildContext(buildContext.contextDir, remoteContextDir, log);
|
|
6045
|
-
await this.buildImageOnRemote(
|
|
6231
|
+
await this.buildImageOnRemote(
|
|
6232
|
+
config,
|
|
6233
|
+
remoteContextDir,
|
|
6234
|
+
buildContext.dockerfileName,
|
|
6235
|
+
tag,
|
|
6236
|
+
log,
|
|
6237
|
+
signal
|
|
6238
|
+
);
|
|
6046
6239
|
} finally {
|
|
6047
6240
|
this.connectionOptions?.executor?.exec(`rm -rf ${sq(remoteContextDir)}`).catch(() => {
|
|
6048
6241
|
});
|
|
@@ -6058,11 +6251,12 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6058
6251
|
* but it's correct for local/TCP where there's no separate SSH
|
|
6059
6252
|
* connection to piggyback on.
|
|
6060
6253
|
*/
|
|
6061
|
-
async buildViaDockerode(config, buildContext, tag, log) {
|
|
6254
|
+
async buildViaDockerode(config, buildContext, tag, log, cancelSignal) {
|
|
6062
6255
|
log.log(`Streaming build context to Docker daemon - image tag: ${tag}`);
|
|
6063
6256
|
const { body, abortSignal, takeError } = packBuildContext(
|
|
6064
6257
|
buildContext.contextDir,
|
|
6065
|
-
buildContext.contextEntries
|
|
6258
|
+
buildContext.contextEntries,
|
|
6259
|
+
cancelSignal
|
|
6066
6260
|
);
|
|
6067
6261
|
try {
|
|
6068
6262
|
const stream = await this.docker.buildImage(body, {
|
|
@@ -6195,7 +6389,17 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6195
6389
|
const log = logger ?? new BuildLogger();
|
|
6196
6390
|
const startTime = Date.now();
|
|
6197
6391
|
const tag = this.imageTag(config.slug, config.sessionId);
|
|
6392
|
+
const abort = registerDockerBuild(config.sessionId);
|
|
6393
|
+
const cancelled = () => {
|
|
6394
|
+
log.step("build", "failed", "Docker build cancelled");
|
|
6395
|
+
return {
|
|
6396
|
+
sessionId: config.sessionId,
|
|
6397
|
+
status: "cancelled",
|
|
6398
|
+
durationMs: Date.now() - startTime
|
|
6399
|
+
};
|
|
6400
|
+
};
|
|
6198
6401
|
try {
|
|
6402
|
+
if (abort.signal.aborted) return cancelled();
|
|
6199
6403
|
log.log(`Build strategy: docker (${this.transport.description})
|
|
6200
6404
|
`);
|
|
6201
6405
|
try {
|
|
@@ -6209,6 +6413,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6209
6413
|
try {
|
|
6210
6414
|
this.emitDockerStep(log, "clone", "running", "Cloning source on the server...");
|
|
6211
6415
|
await this.cloneSourceOnRemote(config, remoteContextDir, log);
|
|
6416
|
+
if (abort.signal.aborted) throw new BuildCancelledError();
|
|
6212
6417
|
this.emitDockerStep(log, "clone", "completed", "Source cloned on the server");
|
|
6213
6418
|
const dockerfileName = await this.resolveRemoteDockerfile(
|
|
6214
6419
|
config,
|
|
@@ -6216,11 +6421,19 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6216
6421
|
"Dockerfile.openship",
|
|
6217
6422
|
config.stack === "docker"
|
|
6218
6423
|
);
|
|
6219
|
-
await this.buildImageOnRemote(
|
|
6424
|
+
await this.buildImageOnRemote(
|
|
6425
|
+
config,
|
|
6426
|
+
remoteContextDir,
|
|
6427
|
+
dockerfileName,
|
|
6428
|
+
tag,
|
|
6429
|
+
log,
|
|
6430
|
+
abort.signal
|
|
6431
|
+
);
|
|
6220
6432
|
} finally {
|
|
6221
6433
|
sshExecutor.exec(`rm -rf ${sq(remoteContextDir)}`).catch(() => {
|
|
6222
6434
|
});
|
|
6223
6435
|
}
|
|
6436
|
+
if (abort.signal.aborted) return cancelled();
|
|
6224
6437
|
await this.verifyImageBuilt(tag);
|
|
6225
6438
|
log.log(`Image ${tag} is ready.
|
|
6226
6439
|
`);
|
|
@@ -6232,6 +6445,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6232
6445
|
requireRepositoryDockerfile: config.stack === "docker",
|
|
6233
6446
|
onLog: log.callback
|
|
6234
6447
|
});
|
|
6448
|
+
if (abort.signal.aborted) throw new BuildCancelledError();
|
|
6235
6449
|
try {
|
|
6236
6450
|
const sizeBytes = await this.estimateContextSize(buildContext.contextDir);
|
|
6237
6451
|
const sizeMB = (sizeBytes / 1024 / 1024).toFixed(1);
|
|
@@ -6268,10 +6482,11 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6268
6482
|
this.emitDockerStep(log, "build", "skipped", "No build command configured");
|
|
6269
6483
|
}
|
|
6270
6484
|
if (sshExecutor) {
|
|
6271
|
-
await this.buildViaSshTarPipe(config, buildContext, tag, log);
|
|
6485
|
+
await this.buildViaSshTarPipe(config, buildContext, tag, log, abort.signal);
|
|
6272
6486
|
} else {
|
|
6273
|
-
await this.buildViaDockerode(config, buildContext, tag, log);
|
|
6487
|
+
await this.buildViaDockerode(config, buildContext, tag, log, abort.signal);
|
|
6274
6488
|
}
|
|
6489
|
+
if (abort.signal.aborted) return cancelled();
|
|
6275
6490
|
await this.verifyImageBuilt(tag);
|
|
6276
6491
|
log.log(`Image ${tag} is ready.
|
|
6277
6492
|
`);
|
|
@@ -6280,9 +6495,12 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6280
6495
|
const durationMs = Date.now() - startTime;
|
|
6281
6496
|
return { sessionId: config.sessionId, status: "deploying", imageRef: tag, durationMs };
|
|
6282
6497
|
} catch (err) {
|
|
6498
|
+
if (abort.signal.aborted || err instanceof BuildCancelledError) return cancelled();
|
|
6283
6499
|
const msg = safeErrorMessage(err);
|
|
6284
6500
|
log.step("build", "failed", `Docker build failed: ${msg}`);
|
|
6285
6501
|
return { sessionId: config.sessionId, status: "failed", durationMs: Date.now() - startTime, errorMessage: `Docker build failed: ${msg}` };
|
|
6502
|
+
} finally {
|
|
6503
|
+
releaseDockerBuild(config.sessionId, abort);
|
|
6286
6504
|
}
|
|
6287
6505
|
}
|
|
6288
6506
|
/**
|
|
@@ -6344,23 +6562,33 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6344
6562
|
}
|
|
6345
6563
|
const tag = buildResult.imageRef;
|
|
6346
6564
|
const extractStart = Date.now();
|
|
6565
|
+
const abort = registerDockerBuild(config.sessionId);
|
|
6566
|
+
const elapsed = () => (buildResult.durationMs ?? 0) + (Date.now() - extractStart);
|
|
6567
|
+
const cancelled = () => {
|
|
6568
|
+
log.step("build", "failed", "Static extract cancelled");
|
|
6569
|
+
return { sessionId: config.sessionId, status: "cancelled", durationMs: elapsed() };
|
|
6570
|
+
};
|
|
6347
6571
|
try {
|
|
6348
6572
|
await this.moveStaticBuildToHost(tag, config, hostOutDir, log);
|
|
6573
|
+
if (abort.signal.aborted) return cancelled();
|
|
6349
6574
|
return {
|
|
6350
6575
|
sessionId: config.sessionId,
|
|
6351
6576
|
status: "deploying",
|
|
6352
6577
|
imageRef: hostOutDir,
|
|
6353
|
-
durationMs: (
|
|
6578
|
+
durationMs: elapsed()
|
|
6354
6579
|
};
|
|
6355
6580
|
} catch (err) {
|
|
6581
|
+
if (abort.signal.aborted || err instanceof BuildCancelledError) return cancelled();
|
|
6356
6582
|
const msg = safeErrorMessage(err);
|
|
6357
6583
|
log.step("build", "failed", `Static extract failed: ${msg}`);
|
|
6358
6584
|
return {
|
|
6359
6585
|
sessionId: config.sessionId,
|
|
6360
6586
|
status: "failed",
|
|
6361
|
-
durationMs: (
|
|
6587
|
+
durationMs: elapsed(),
|
|
6362
6588
|
errorMessage: `Static extract failed: ${msg}`
|
|
6363
6589
|
};
|
|
6590
|
+
} finally {
|
|
6591
|
+
releaseDockerBuild(config.sessionId, abort);
|
|
6364
6592
|
}
|
|
6365
6593
|
}
|
|
6366
6594
|
/**
|
|
@@ -6394,6 +6622,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6394
6622
|
async extractDocRootOverSsh(sshExecutor, tag, docRoot, hostOutDir) {
|
|
6395
6623
|
const cid = (await sshExecutor.exec(`docker create ${sq(tag)}`)).trim();
|
|
6396
6624
|
try {
|
|
6625
|
+
await ensureOwnedDir(sshExecutor, dirOf(hostOutDir));
|
|
6397
6626
|
await sshExecutor.exec(`mkdir -p ${sq(hostOutDir)}`);
|
|
6398
6627
|
await sshExecutor.exec(`docker cp ${sq(`${cid}:${docRoot}/.`)} ${sq(hostOutDir)}`);
|
|
6399
6628
|
const listing = (await sshExecutor.exec(`ls -A ${sq(hostOutDir)} | head -1`).catch(() => "")).trim();
|
|
@@ -6542,16 +6771,34 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6542
6771
|
const isSsh = this.transport.kind === "ssh" && !!this.connectionOptions?.executor;
|
|
6543
6772
|
const cloneOnServer = isSsh && !!source.cloneOnServer;
|
|
6544
6773
|
const remoteContextDir = `/tmp/openship-build-${source.sessionId}`;
|
|
6774
|
+
const abortControllers = new Map(
|
|
6775
|
+
specs.map((spec) => [spec.config.sessionId, registerDockerBuild(spec.config.sessionId)])
|
|
6776
|
+
);
|
|
6777
|
+
const isCancelled = (sessionId) => abortControllers.get(sessionId)?.signal.aborted === true;
|
|
6778
|
+
const allCancelled = () => [...abortControllers.values()].every((c) => c.signal.aborted);
|
|
6779
|
+
const cancelledResult = (sessionId, startedAt) => ({
|
|
6780
|
+
sessionId,
|
|
6781
|
+
status: "cancelled",
|
|
6782
|
+
durationMs: Date.now() - startedAt
|
|
6783
|
+
});
|
|
6545
6784
|
let tree = null;
|
|
6546
|
-
if (cloneOnServer) {
|
|
6547
|
-
prepareLogger.step("clone", "running", "Cloning source on the server...");
|
|
6548
|
-
await this.cloneSourceOnRemote(source, remoteContextDir, prepareLogger);
|
|
6549
|
-
prepareLogger.step("clone", "completed", "Source cloned on the server");
|
|
6550
|
-
} else {
|
|
6551
|
-
prepareLogger.step("clone", "running", "Preparing shared build context...");
|
|
6552
|
-
tree = await prepareSourceTree(source, { onLog: prepareLogger.callback });
|
|
6553
|
-
}
|
|
6554
6785
|
try {
|
|
6786
|
+
if (allCancelled()) {
|
|
6787
|
+
const startedAt = Date.now();
|
|
6788
|
+
return specs.map((spec) => {
|
|
6789
|
+
const result = cancelledResult(spec.config.sessionId, startedAt);
|
|
6790
|
+
spec.onResult?.(result);
|
|
6791
|
+
return { serviceName: spec.serviceName, result };
|
|
6792
|
+
});
|
|
6793
|
+
}
|
|
6794
|
+
if (cloneOnServer) {
|
|
6795
|
+
prepareLogger.step("clone", "running", "Cloning source on the server...");
|
|
6796
|
+
await this.cloneSourceOnRemote(source, remoteContextDir, prepareLogger);
|
|
6797
|
+
prepareLogger.step("clone", "completed", "Source cloned on the server");
|
|
6798
|
+
} else {
|
|
6799
|
+
prepareLogger.step("clone", "running", "Preparing shared build context...");
|
|
6800
|
+
tree = await prepareSourceTree(source, { onLog: prepareLogger.callback });
|
|
6801
|
+
}
|
|
6555
6802
|
const resolvedList = await Promise.all(
|
|
6556
6803
|
specs.map(async (spec) => {
|
|
6557
6804
|
const generatedName = `Dockerfile.openship.${spec.config.sessionId}`;
|
|
@@ -6602,7 +6849,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6602
6849
|
} catch {
|
|
6603
6850
|
prepareLogger.step("clone", "completed", "Shared build context ready");
|
|
6604
6851
|
}
|
|
6605
|
-
if (isSsh) {
|
|
6852
|
+
if (isSsh && !allCancelled()) {
|
|
6606
6853
|
await this.transferBuildContext(tree.contextDir, remoteContextDir, prepareLogger);
|
|
6607
6854
|
}
|
|
6608
6855
|
}
|
|
@@ -6610,6 +6857,13 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6610
6857
|
for (const { spec, dockerfileName, contextEntries, error } of resolvedList) {
|
|
6611
6858
|
const startedAt = Date.now();
|
|
6612
6859
|
const tag = this.imageTag(spec.config.slug, spec.config.sessionId);
|
|
6860
|
+
const signal = abortControllers.get(spec.config.sessionId)?.signal;
|
|
6861
|
+
if (isCancelled(spec.config.sessionId)) {
|
|
6862
|
+
const result = cancelledResult(spec.config.sessionId, startedAt);
|
|
6863
|
+
spec.onResult?.(result);
|
|
6864
|
+
results.push({ serviceName: spec.serviceName, result });
|
|
6865
|
+
continue;
|
|
6866
|
+
}
|
|
6613
6867
|
if (error || !dockerfileName) {
|
|
6614
6868
|
const result = {
|
|
6615
6869
|
sessionId: spec.config.sessionId,
|
|
@@ -6629,12 +6883,14 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6629
6883
|
remoteContextDir,
|
|
6630
6884
|
dockerfileName,
|
|
6631
6885
|
tag,
|
|
6632
|
-
spec.logger
|
|
6886
|
+
spec.logger,
|
|
6887
|
+
signal
|
|
6633
6888
|
);
|
|
6634
6889
|
} else {
|
|
6635
6890
|
const { body, abortSignal, takeError } = packBuildContext(
|
|
6636
6891
|
tree.contextDir,
|
|
6637
|
-
contextEntries ?? []
|
|
6892
|
+
contextEntries ?? [],
|
|
6893
|
+
signal
|
|
6638
6894
|
);
|
|
6639
6895
|
try {
|
|
6640
6896
|
const stream = await this.docker.buildImage(body, {
|
|
@@ -6655,6 +6911,12 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6655
6911
|
throw contextErr ? new Error(`Failed to read Docker build context: ${safeErrorMessage(contextErr)}`, { cause: contextErr }) : err;
|
|
6656
6912
|
}
|
|
6657
6913
|
}
|
|
6914
|
+
if (isCancelled(spec.config.sessionId)) {
|
|
6915
|
+
const result2 = cancelledResult(spec.config.sessionId, startedAt);
|
|
6916
|
+
spec.onResult?.(result2);
|
|
6917
|
+
results.push({ serviceName: spec.serviceName, result: result2 });
|
|
6918
|
+
continue;
|
|
6919
|
+
}
|
|
6658
6920
|
await this.verifyImageBuilt(tag);
|
|
6659
6921
|
if (spec.config.staticExtractOnly && spec.config.staticOutDir) {
|
|
6660
6922
|
await this.moveStaticBuildToHost(
|
|
@@ -6663,7 +6925,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6663
6925
|
spec.config.staticOutDir,
|
|
6664
6926
|
spec.logger
|
|
6665
6927
|
);
|
|
6666
|
-
const result2 = {
|
|
6928
|
+
const result2 = isCancelled(spec.config.sessionId) ? cancelledResult(spec.config.sessionId, startedAt) : {
|
|
6667
6929
|
sessionId: spec.config.sessionId,
|
|
6668
6930
|
status: "deploying",
|
|
6669
6931
|
imageRef: spec.config.staticOutDir,
|
|
@@ -6684,6 +6946,13 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6684
6946
|
spec.onResult?.(result);
|
|
6685
6947
|
results.push({ serviceName: spec.serviceName, result });
|
|
6686
6948
|
} catch (err) {
|
|
6949
|
+
if (isCancelled(spec.config.sessionId) || err instanceof BuildCancelledError) {
|
|
6950
|
+
spec.logger.log("Docker build cancelled\n", "error");
|
|
6951
|
+
const result2 = cancelledResult(spec.config.sessionId, startedAt);
|
|
6952
|
+
spec.onResult?.(result2);
|
|
6953
|
+
results.push({ serviceName: spec.serviceName, result: result2 });
|
|
6954
|
+
continue;
|
|
6955
|
+
}
|
|
6687
6956
|
const msg = safeErrorMessage(err);
|
|
6688
6957
|
spec.logger.log(`Docker build failed: ${msg}
|
|
6689
6958
|
`, "error");
|
|
@@ -6699,6 +6968,9 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6699
6968
|
}
|
|
6700
6969
|
return results;
|
|
6701
6970
|
} finally {
|
|
6971
|
+
for (const [sessionId, abort] of abortControllers) {
|
|
6972
|
+
releaseDockerBuild(sessionId, abort);
|
|
6973
|
+
}
|
|
6702
6974
|
if (isSsh) {
|
|
6703
6975
|
this.connectionOptions?.executor?.exec(`rm -rf ${sq(remoteContextDir)}`).catch(() => {
|
|
6704
6976
|
});
|
|
@@ -6707,11 +6979,23 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6707
6979
|
}
|
|
6708
6980
|
}
|
|
6709
6981
|
async cancelBuild(sessionId) {
|
|
6982
|
+
for (const [activeId, abort] of activeDockerBuilds) {
|
|
6983
|
+
if (cancelCovers(sessionId, activeId)) abort.abort();
|
|
6984
|
+
}
|
|
6985
|
+
recordPendingCancellation(sessionId);
|
|
6986
|
+
const executor = this.transport.kind === "ssh" ? this.connectionOptions?.executor : void 0;
|
|
6987
|
+
if (executor) {
|
|
6988
|
+
await killProcessesUnderDir(executor, `/tmp/openship-build-${sessionId}`, {
|
|
6989
|
+
includeSuffixed: true
|
|
6990
|
+
});
|
|
6991
|
+
}
|
|
6710
6992
|
const containers = await this.docker.listContainers({
|
|
6711
6993
|
all: true,
|
|
6712
|
-
filters: { label: [
|
|
6994
|
+
filters: { label: ["openship.build"] }
|
|
6713
6995
|
});
|
|
6714
6996
|
for (const c of containers) {
|
|
6997
|
+
const buildId = c.Labels?.["openship.build"];
|
|
6998
|
+
if (!buildId || !cancelCovers(sessionId, buildId)) continue;
|
|
6715
6999
|
try {
|
|
6716
7000
|
await this.docker.getContainer(c.Id).remove({ force: true });
|
|
6717
7001
|
} catch {
|
|
@@ -6732,7 +7016,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6732
7016
|
}
|
|
6733
7017
|
const containerName = `openship-${config.runtimeName || config.projectId}-${config.deploymentId}`;
|
|
6734
7018
|
const env = [
|
|
6735
|
-
`PORT=${config.port}
|
|
7019
|
+
...config.portless ? [] : [`PORT=${config.port}`],
|
|
6736
7020
|
`NODE_ENV=${config.environment === "production" ? "production" : "development"}`,
|
|
6737
7021
|
...Object.entries(config.envVars).map(([k, v]) => `${k}=${v}`)
|
|
6738
7022
|
];
|
|
@@ -6796,7 +7080,9 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6796
7080
|
// rebuild, exactly like a compose service.
|
|
6797
7081
|
...networkId ? { service: config.networkAlias } : {}
|
|
6798
7082
|
}),
|
|
6799
|
-
|
|
7083
|
+
// A worker exposes and publishes no port (#538-B); everything else exposes
|
|
7084
|
+
// its app port for the loopback publish below.
|
|
7085
|
+
...config.portless ? {} : { ExposedPorts: { [`${config.port}/tcp`]: {} } },
|
|
6800
7086
|
...networkId ? { NetworkingConfig: { EndpointsConfig: { [networkId]: { Aliases: aliases } } } } : {},
|
|
6801
7087
|
HostConfig: {
|
|
6802
7088
|
RestartPolicy: restartPolicy,
|
|
@@ -6814,10 +7100,13 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6814
7100
|
// app directly, bypassing the edge's SSL/rate-limit/rules (and Docker's
|
|
6815
7101
|
// iptables bypass ufw). A pinned `config.hostPort` (loopback-port route
|
|
6816
7102
|
// strategy) is stable across redeploys; otherwise a random loopback port.
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
|
|
6820
|
-
|
|
7103
|
+
// A worker (config.portless) binds no host port at all (#538-B).
|
|
7104
|
+
...config.portless ? {} : {
|
|
7105
|
+
PortBindings: {
|
|
7106
|
+
[`${config.port}/tcp`]: [
|
|
7107
|
+
{ HostIp: "127.0.0.1", HostPort: config.hostPort ? String(config.hostPort) : "" }
|
|
7108
|
+
]
|
|
7109
|
+
}
|
|
6821
7110
|
}
|
|
6822
7111
|
}
|
|
6823
7112
|
});
|
|
@@ -6869,6 +7158,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6869
7158
|
}
|
|
6870
7159
|
const container = this.docker.getContainer(containerId);
|
|
6871
7160
|
try {
|
|
7161
|
+
await gracefulStopForGrace(container);
|
|
6872
7162
|
await container.remove({ force: true });
|
|
6873
7163
|
} catch (err) {
|
|
6874
7164
|
if (!isDockerNotFoundError(err)) throw err;
|
|
@@ -7731,16 +8021,17 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7731
8021
|
const network = this.docker.getNetwork(networkId);
|
|
7732
8022
|
for (const m of members) {
|
|
7733
8023
|
if (!m.containerId) continue;
|
|
8024
|
+
const aliases = m.aliases.filter(Boolean);
|
|
7734
8025
|
try {
|
|
7735
8026
|
await network.connect({
|
|
7736
8027
|
Container: m.containerId,
|
|
7737
|
-
EndpointConfig:
|
|
8028
|
+
EndpointConfig: aliases.length ? { Aliases: aliases } : {}
|
|
7738
8029
|
});
|
|
7739
8030
|
} catch (err) {
|
|
7740
8031
|
const msg = err?.message ?? "";
|
|
7741
8032
|
if (!/already exists|already connected/i.test(msg)) {
|
|
7742
8033
|
console.warn(
|
|
7743
|
-
`[docker] group join failed for ${m.containerId.slice(0, 12)} (${
|
|
8034
|
+
`[docker] group join failed for ${m.containerId.slice(0, 12)} (${aliases.join(", ")}): ${msg}`
|
|
7744
8035
|
);
|
|
7745
8036
|
}
|
|
7746
8037
|
}
|
|
@@ -7802,6 +8093,20 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7802
8093
|
* the narrow case where the network's identity changed out-of-band (docker
|
|
7803
8094
|
* network prune/rm, daemon/host rebuild) and surviving containers fell off it.
|
|
7804
8095
|
*/
|
|
8096
|
+
/**
|
|
8097
|
+
* A container that can never be attached to a network, because it doesn't own a
|
|
8098
|
+
* network namespace to attach (compose `network_mode: container:…`/`none`, or a
|
|
8099
|
+
* migration-adopted `host`).
|
|
8100
|
+
*
|
|
8101
|
+
* Both membership loops below are best-effort and swallow errors, so without this
|
|
8102
|
+
* they'd still work — they'd just log "connect failed" for such a container on
|
|
8103
|
+
* every single deploy, forever, over something that is correct by design. Read
|
|
8104
|
+
* from `HostConfig.NetworkMode`, which `/containers/json` reports.
|
|
8105
|
+
*/
|
|
8106
|
+
cannotJoinNetworks(container) {
|
|
8107
|
+
const mode = container.HostConfig?.NetworkMode;
|
|
8108
|
+
return !!mode && (mode.startsWith("container:") || mode === "none" || mode === "host");
|
|
8109
|
+
}
|
|
7805
8110
|
async reconcileNetworkMembership(networkId, projectId) {
|
|
7806
8111
|
let containers;
|
|
7807
8112
|
try {
|
|
@@ -7818,6 +8123,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7818
8123
|
(n) => n?.NetworkID === networkId
|
|
7819
8124
|
);
|
|
7820
8125
|
if (onNetwork) continue;
|
|
8126
|
+
if (this.cannotJoinNetworks(c)) continue;
|
|
7821
8127
|
const service = c.Labels?.["openship.service"];
|
|
7822
8128
|
try {
|
|
7823
8129
|
await network.connect({
|
|
@@ -7867,6 +8173,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7867
8173
|
(n) => n?.NetworkID === netId
|
|
7868
8174
|
);
|
|
7869
8175
|
if (onNetwork) continue;
|
|
8176
|
+
if (this.cannotJoinNetworks(c)) continue;
|
|
7870
8177
|
try {
|
|
7871
8178
|
await network.connect({ Container: c.Id });
|
|
7872
8179
|
} catch (err) {
|
|
@@ -7901,6 +8208,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7901
8208
|
const containerName = `openship-${config.slug}-${config.serviceName}`;
|
|
7902
8209
|
try {
|
|
7903
8210
|
const existing = this.docker.getContainer(containerName);
|
|
8211
|
+
await gracefulStopForGrace(existing);
|
|
7904
8212
|
await existing.remove({ force: true });
|
|
7905
8213
|
} catch {
|
|
7906
8214
|
}
|
|
@@ -7909,6 +8217,10 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7909
8217
|
...Object.entries(config.environment).map(([k, v]) => `${k}=${v}`)
|
|
7910
8218
|
];
|
|
7911
8219
|
const cmd = resolveComposeCmd(config);
|
|
8220
|
+
const sharedNetwork = config.namespaces?.network;
|
|
8221
|
+
const sharesNetns = !!sharedNetwork && sharedNetwork !== "none";
|
|
8222
|
+
const ownsProjectEndpoint = ownsNetworkEndpoint(sharedNetwork);
|
|
8223
|
+
const sharedPid = config.namespaces?.pid;
|
|
7912
8224
|
const { exposedPorts, portBindings } = parsePortBindings(config.ports);
|
|
7913
8225
|
if (!config.namespaceVolumes) {
|
|
7914
8226
|
await this.assertNoForeignNamedVolumeCollision(config);
|
|
@@ -7917,6 +8229,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7917
8229
|
const binds = scopedBinds.length > 0 ? scopedBinds : void 0;
|
|
7918
8230
|
const restartPolicy = resolveRestartPolicy(config.restart);
|
|
7919
8231
|
const healthcheck = toDockerHealthcheck(config.advanced?.healthcheck);
|
|
8232
|
+
const stopConfig = toStopConfig(config.advanced);
|
|
7920
8233
|
log({
|
|
7921
8234
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7922
8235
|
message: `Creating service container ${containerName} from ${config.image}...
|
|
@@ -7929,6 +8242,23 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7929
8242
|
`,
|
|
7930
8243
|
level: "info"
|
|
7931
8244
|
});
|
|
8245
|
+
if (sharedNetwork) {
|
|
8246
|
+
log({
|
|
8247
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8248
|
+
message: `Network namespace: ${sharedNetwork} \u2014 this container gets no interface of its own` + (sharesNetns ? `, so its ports, project-network alias, and hostname come from the service it shares. Publish and route on THAT service instead.
|
|
8249
|
+
` : ` and no network access at all.
|
|
8250
|
+
`),
|
|
8251
|
+
level: "warn"
|
|
8252
|
+
});
|
|
8253
|
+
}
|
|
8254
|
+
if (sharedPid) {
|
|
8255
|
+
log({
|
|
8256
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8257
|
+
message: `PID namespace: ${sharedPid}
|
|
8258
|
+
`,
|
|
8259
|
+
level: "info"
|
|
8260
|
+
});
|
|
8261
|
+
}
|
|
7932
8262
|
if (!config.image.startsWith("openship/")) {
|
|
7933
8263
|
try {
|
|
7934
8264
|
log({
|
|
@@ -7953,7 +8283,9 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7953
8283
|
Image: config.image,
|
|
7954
8284
|
Cmd: cmd,
|
|
7955
8285
|
Env: env,
|
|
7956
|
-
|
|
8286
|
+
// Docker rejects an explicit hostname when the network namespace is another
|
|
8287
|
+
// container's — that container owns the UTS identity too.
|
|
8288
|
+
...sharesNetns ? {} : { Hostname: config.serviceName },
|
|
7957
8289
|
Labels: {
|
|
7958
8290
|
...this.labels({
|
|
7959
8291
|
deploymentId: config.deploymentId,
|
|
@@ -7962,23 +8294,31 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7962
8294
|
"openship.service": config.serviceName
|
|
7963
8295
|
},
|
|
7964
8296
|
...healthcheck && { Healthcheck: healthcheck },
|
|
7965
|
-
|
|
8297
|
+
...stopConfig,
|
|
8298
|
+
...ownsProjectEndpoint ? { ExposedPorts: exposedPorts } : {},
|
|
7966
8299
|
HostConfig: {
|
|
7967
8300
|
RestartPolicy: restartPolicy,
|
|
7968
8301
|
...dockerResourceLimits(config.resources),
|
|
7969
|
-
PortBindings: portBindings,
|
|
8302
|
+
...ownsProjectEndpoint ? { PortBindings: portBindings } : {},
|
|
7970
8303
|
Binds: binds,
|
|
7971
|
-
|
|
8304
|
+
// The project network is the default; a compose `network_mode` replaces it.
|
|
8305
|
+
NetworkMode: sharedNetwork ?? group.id,
|
|
8306
|
+
...sharedPid ? { PidMode: sharedPid } : {}
|
|
7972
8307
|
},
|
|
7973
|
-
|
|
7974
|
-
|
|
7975
|
-
|
|
7976
|
-
|
|
7977
|
-
|
|
7978
|
-
|
|
8308
|
+
// A declared network mode means there is no endpoint to name — omit the
|
|
8309
|
+
// block entirely rather than send an empty one (and never contradict
|
|
8310
|
+
// `NetworkMode: none` by attaching a network anyway).
|
|
8311
|
+
...ownsProjectEndpoint ? {
|
|
8312
|
+
NetworkingConfig: {
|
|
8313
|
+
EndpointsConfig: {
|
|
8314
|
+
[group.id]: {
|
|
8315
|
+
// Default service name plus any operator-chosen aliases; both
|
|
8316
|
+
// resolve to this container on the project network.
|
|
8317
|
+
Aliases: buildNetworkAliases(config.serviceName, config.extraAliases)
|
|
8318
|
+
}
|
|
7979
8319
|
}
|
|
7980
8320
|
}
|
|
7981
|
-
}
|
|
8321
|
+
} : {}
|
|
7982
8322
|
});
|
|
7983
8323
|
try {
|
|
7984
8324
|
await container.start();
|
|
@@ -8034,12 +8374,17 @@ export {
|
|
|
8034
8374
|
startExecStream,
|
|
8035
8375
|
startAttachStream,
|
|
8036
8376
|
daemonConnectionFrom,
|
|
8377
|
+
DEFAULT_DOCKER_SOCKET_PATH,
|
|
8378
|
+
resolveLocalDockerSocketPath,
|
|
8037
8379
|
ownsBuiltImage,
|
|
8038
8380
|
buildInContainerExecCmd,
|
|
8039
8381
|
resolveExecExitCode,
|
|
8040
8382
|
buildNetworkAliases,
|
|
8041
8383
|
parsePortBindings,
|
|
8042
8384
|
packBuildContext,
|
|
8385
|
+
toDockerHealthcheck,
|
|
8386
|
+
toStopConfig,
|
|
8387
|
+
gracefulStopForGrace,
|
|
8043
8388
|
splitDockerFrames,
|
|
8044
8389
|
parseContainerEventLine,
|
|
8045
8390
|
DockerRuntime
|