openship 0.6.1 → 0.6.6
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-IWS2QHJ3.js → bare-OXJVW3AK.js} +11 -9
- package/dist/{chunk-VZ2ZHPS2.js → chunk-2YM3NI2E.js} +3 -9
- package/dist/chunk-4I2BM2DT.js +68 -0
- package/dist/{chunk-2NZSUXOB.js → chunk-5DMWSTOM.js} +310 -1459
- package/dist/{chunk-3FYIXVX5.js → chunk-77QDF6UD.js} +581 -122
- package/dist/{chunk-IOT5ZX23.js → chunk-A4GXYE4L.js} +449 -383
- package/dist/chunk-CBLD2HS5.js +428 -0
- package/dist/chunk-ELGRMZOW.js +25 -0
- package/dist/{chunk-TWJCLD4W.js → chunk-FE7ZZQSE.js} +5 -5
- package/dist/{chunk-3NZICQHF.js → chunk-HKRQXPTI.js} +31 -8
- package/dist/{chunk-IXM5245F.js → chunk-LFE5KQ7Y.js} +553 -538
- package/dist/{chunk-VDDMADOF.js → chunk-LH2PLRQO.js} +16 -2
- package/dist/{chunk-EMXI4UCU.js → chunk-LUKYPVJ4.js} +29 -10
- package/dist/{chunk-UC7YIEHT.js → chunk-MLYSEZQB.js} +352 -47
- package/dist/chunk-NK5N5LXN.js +1681 -0
- package/dist/{chunk-RRJJGVKE.js → chunk-OBPKYXQL.js} +75 -17
- package/dist/{chunk-6ODUBKUB.js → chunk-PCPVTH6M.js} +78 -88
- package/dist/chunk-Q7WGTQQ5.js +88 -0
- package/dist/{chunk-EW6KB2J7.js → chunk-QVW76WDL.js} +440 -74
- package/dist/{chunk-YIPMDGG6.js → chunk-RSR7PFIQ.js} +79 -23
- package/dist/{chunk-3LDWWY7U.js → chunk-SM26HDS2.js} +41 -7
- package/dist/{chunk-OTLSJYAC.js → chunk-UJ6JZM65.js} +5185 -341
- package/dist/chunk-UVNMQM7W.js +444 -0
- package/dist/chunk-VTGPEC6B.js +308 -0
- package/dist/chunk-WVAML4H6.js +2145 -0
- package/dist/{chunk-JIMUYEKN.js → chunk-YIMOCZBZ.js} +1 -1
- package/dist/chunk-YMMQRE5J.js +292 -0
- package/dist/cloud-75VIT4KV.js +37 -0
- package/dist/{detect-ALF4GXRX.js → detect-OIE5V3VZ.js} +3 -3
- package/dist/{docker-QECQJ5P4.js → docker-VTR2GK5Y.js} +16 -8
- package/dist/{docker-edge-executor-62N64MGS.js → docker-edge-executor-22IBXNYJ.js} +5 -5
- package/dist/{edge-import-SAYTRJLO.js → edge-import-WJUB4M7F.js} +27 -23
- package/dist/edge-preflight-GBNCPNVU.js +42 -0
- package/dist/{ensure-container-edge-CFSCPGZK.js → ensure-container-edge-KPI2CVXJ.js} +9 -8
- package/dist/{executor-PT6UEJ5M.js → executor-7LXXZPBR.js} +15 -5
- package/dist/host-channel-preflight-KE6OONYK.js +50 -0
- package/dist/index.js +3783 -4221
- package/dist/{nginx-YOEEFBTE.js → nginx-KUFMLMAC.js} +13 -5
- package/dist/node-bootstrap.js +241 -0
- package/dist/node-entry.js +52 -1
- package/dist/{openresty-lua-WGBO2M3Z.js → openresty-lua-ATTH35BE.js} +8 -2
- package/dist/privilege-I4W3ZNZK.js +19 -0
- package/dist/server/engine/samples/amavisd/amavisd.conf +25 -0
- package/dist/server/engine/samples/fail2ban/jail.d/sshd.local +4 -1
- package/dist/server/engine/samples/postfix/master.cf +14 -0
- package/dist/server/index.js +77659 -61909
- 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/0105_mail_inbound_rules.sql +80 -0
- package/dist/server/migrations/0106_mcp_call_tracking.sql +23 -0
- package/dist/server/migrations/0107_build_minute_metering_indexes.sql +20 -0
- package/dist/server/migrations/0108_credential.sql +68 -0
- package/dist/server/migrations/meta/_journal.json +77 -0
- package/dist/{setup-M2ZDGH3O.js → setup-XGLGUUFW.js} +10 -9
- package/dist/ssh-client-JBMA7SN4.js +28 -0
- package/dist/up-K4BV7466.js +57 -0
- package/package.json +4 -2
- package/dist/chunk-IW2UKBVF.js +0 -144
- package/dist/chunk-ZCFVHA25.js +0 -89
- package/dist/cloud-M2YLWEXO.js +0 -31
- /package/dist/{dist-es-JGBPABJI.js → dist-es-AXFRS7PS.js} +0 -0
|
@@ -9,33 +9,43 @@ import {
|
|
|
9
9
|
resolveDockerfileCandidates,
|
|
10
10
|
resolveServiceDockerfile,
|
|
11
11
|
staticBuilderOutputPath
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-LUKYPVJ4.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-LH2PLRQO.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-PCPVTH6M.js";
|
|
28
33
|
import {
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
dirOf,
|
|
35
|
+
ensureOwnedDir
|
|
36
|
+
} from "./chunk-UVNMQM7W.js";
|
|
31
37
|
import {
|
|
32
38
|
formatCpuCores,
|
|
33
39
|
formatMemoryMb,
|
|
34
40
|
hasCpuLimit,
|
|
35
41
|
hasMemoryLimit,
|
|
42
|
+
isRuntimeNotFoundError,
|
|
43
|
+
ownsNetworkEndpoint,
|
|
44
|
+
registryConfigKeys,
|
|
45
|
+
registryForImage,
|
|
36
46
|
safeErrorMessage,
|
|
37
47
|
withTimeout
|
|
38
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-UJ6JZM65.js";
|
|
39
49
|
import {
|
|
40
50
|
__commonJS,
|
|
41
51
|
__require,
|
|
@@ -305,7 +315,7 @@ var require_buffer_list = __commonJS({
|
|
|
305
315
|
}
|
|
306
316
|
}, {
|
|
307
317
|
key: "join",
|
|
308
|
-
value: function
|
|
318
|
+
value: function join3(s) {
|
|
309
319
|
if (this.length === 0) return "";
|
|
310
320
|
var p = this.head;
|
|
311
321
|
var ret = "" + p.data;
|
|
@@ -4600,6 +4610,82 @@ import Dockerode from "dockerode";
|
|
|
4600
4610
|
import { createGzip } from "zlib";
|
|
4601
4611
|
import { PassThrough, Writable } from "stream";
|
|
4602
4612
|
|
|
4613
|
+
// ../../packages/adapters/src/runtime/docker-auth.ts
|
|
4614
|
+
import { readFile } from "fs/promises";
|
|
4615
|
+
import { homedir } from "os";
|
|
4616
|
+
import { join } from "path";
|
|
4617
|
+
function firstPresent(values, keys) {
|
|
4618
|
+
if (!values) return void 0;
|
|
4619
|
+
for (const key of keys) {
|
|
4620
|
+
if (Object.hasOwn(values, key)) return { key, value: values[key] };
|
|
4621
|
+
}
|
|
4622
|
+
return void 0;
|
|
4623
|
+
}
|
|
4624
|
+
function dockerConfigJsonFor(auth) {
|
|
4625
|
+
const a = auth;
|
|
4626
|
+
const registry = a.serveraddress?.trim();
|
|
4627
|
+
if (!registry) return null;
|
|
4628
|
+
if (a.identitytoken) {
|
|
4629
|
+
return JSON.stringify({ auths: { [registry]: { identitytoken: a.identitytoken } } });
|
|
4630
|
+
}
|
|
4631
|
+
if (!a.username || !a.password) return null;
|
|
4632
|
+
const encoded = Buffer.from(`${a.username}:${a.password}`).toString("base64");
|
|
4633
|
+
return JSON.stringify({ auths: { [registry]: { auth: encoded } } });
|
|
4634
|
+
}
|
|
4635
|
+
function toAuthConfig(entry, serveraddress) {
|
|
4636
|
+
if (entry.identitytoken) {
|
|
4637
|
+
return { identitytoken: entry.identitytoken, serveraddress };
|
|
4638
|
+
}
|
|
4639
|
+
if (entry.username && entry.password) {
|
|
4640
|
+
return { username: entry.username, password: entry.password, serveraddress };
|
|
4641
|
+
}
|
|
4642
|
+
if (!entry.auth) return void 0;
|
|
4643
|
+
let decoded;
|
|
4644
|
+
try {
|
|
4645
|
+
decoded = Buffer.from(entry.auth, "base64").toString("utf8");
|
|
4646
|
+
} catch {
|
|
4647
|
+
return void 0;
|
|
4648
|
+
}
|
|
4649
|
+
const separator = decoded.indexOf(":");
|
|
4650
|
+
if (separator < 1) return void 0;
|
|
4651
|
+
return {
|
|
4652
|
+
username: decoded.slice(0, separator),
|
|
4653
|
+
password: decoded.slice(separator + 1),
|
|
4654
|
+
serveraddress
|
|
4655
|
+
};
|
|
4656
|
+
}
|
|
4657
|
+
var DockerCredentialHelperError = class extends Error {
|
|
4658
|
+
constructor(registry) {
|
|
4659
|
+
super(
|
|
4660
|
+
`Openship can't read the credentials for ${registry}: they are stored in a Docker credential helper, which is not available inside the Openship container. Add the credentials inline to the Docker config (docker logout ${registry}, remove the credential helper for it, then docker login ${registry}), or use a public image.`
|
|
4661
|
+
);
|
|
4662
|
+
this.name = "DockerCredentialHelperError";
|
|
4663
|
+
}
|
|
4664
|
+
};
|
|
4665
|
+
async function resolveDockerAuth(ref) {
|
|
4666
|
+
const registry = registryForImage(ref);
|
|
4667
|
+
const configPath = join(
|
|
4668
|
+
process.env.DOCKER_CONFIG || join(homedir(), ".docker"),
|
|
4669
|
+
"config.json"
|
|
4670
|
+
);
|
|
4671
|
+
let config;
|
|
4672
|
+
try {
|
|
4673
|
+
config = JSON.parse(await readFile(configPath, "utf8"));
|
|
4674
|
+
} catch {
|
|
4675
|
+
return void 0;
|
|
4676
|
+
}
|
|
4677
|
+
if (!config || typeof config !== "object") return void 0;
|
|
4678
|
+
const keys = registryConfigKeys(registry);
|
|
4679
|
+
const inline = firstPresent(config.auths, keys);
|
|
4680
|
+
const usable = inline ? toAuthConfig(inline.value, inline.key) : void 0;
|
|
4681
|
+
if (usable) return usable;
|
|
4682
|
+
const helperForRegistry = firstPresent(config.credHelpers, keys);
|
|
4683
|
+
if (helperForRegistry || config.credsStore && inline) {
|
|
4684
|
+
throw new DockerCredentialHelperError(registry);
|
|
4685
|
+
}
|
|
4686
|
+
return void 0;
|
|
4687
|
+
}
|
|
4688
|
+
|
|
4603
4689
|
// ../../packages/adapters/src/runtime/source-tarball.ts
|
|
4604
4690
|
function githubTarballUrl(repoUrl, ref) {
|
|
4605
4691
|
if (!repoUrl || !ref) return null;
|
|
@@ -4784,6 +4870,9 @@ function resolveComposeCmd(config) {
|
|
|
4784
4870
|
if (config.commandArgv != null) return config.commandArgv;
|
|
4785
4871
|
return config.command ? ["sh", "-c", config.command] : void 0;
|
|
4786
4872
|
}
|
|
4873
|
+
function resolveComposeEntrypoint(advanced) {
|
|
4874
|
+
return advanced?.entrypoint ?? void 0;
|
|
4875
|
+
}
|
|
4787
4876
|
|
|
4788
4877
|
// ../../packages/adapters/src/runtime/resource-limits.ts
|
|
4789
4878
|
function dockerResourceLimits(resources) {
|
|
@@ -4835,16 +4924,16 @@ function describeResourceLimits(resources) {
|
|
|
4835
4924
|
// ../../packages/adapters/src/runtime/docker-transport.ts
|
|
4836
4925
|
import { createHash } from "crypto";
|
|
4837
4926
|
import { readFileSync } from "fs";
|
|
4838
|
-
import { homedir } from "os";
|
|
4839
|
-
import { join } from "path";
|
|
4927
|
+
import { homedir as homedir2 } from "os";
|
|
4928
|
+
import { join as join2 } from "path";
|
|
4840
4929
|
|
|
4841
4930
|
// ../../packages/adapters/src/runtime/docker-ssh-agent.ts
|
|
4842
4931
|
import net2 from "net";
|
|
4843
4932
|
var DEFAULT_REMOTE_DOCKER_SOCKET_PATH = "/var/run/docker.sock";
|
|
4844
4933
|
var resolvedDockerSocketPathCache = /* @__PURE__ */ new WeakMap();
|
|
4845
|
-
var DOCKER_DIAL_STDIO_COMMAND = "docker system dial-stdio";
|
|
4846
4934
|
var STREAMLOCAL_PROBE_TIMEOUT_MS = 8e3;
|
|
4847
4935
|
var STREAMLOCAL_DATA_VERIFY_TIMEOUT_MS = 3e3;
|
|
4936
|
+
var DIAL_STDIO_ANSWER_TIMEOUT_MS = 3e3;
|
|
4848
4937
|
var VERIFY_BUFFER_CAP_BYTES = 8 * 1024 * 1024;
|
|
4849
4938
|
function toSshConfig(opts) {
|
|
4850
4939
|
return {
|
|
@@ -4895,12 +4984,8 @@ async function discoverRemoteDockerSocketPathsWithClient(client) {
|
|
|
4895
4984
|
return normalizeSocketPathLines(lines);
|
|
4896
4985
|
}
|
|
4897
4986
|
async function discoverRemoteDockerSocketPathsWithExecutor(executor) {
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
return normalizeSocketPathLines(output.split(/\r?\n/));
|
|
4901
|
-
} catch {
|
|
4902
|
-
return [];
|
|
4903
|
-
}
|
|
4987
|
+
const output = await executor.exec(DOCKER_SOCKET_DISCOVERY_SCRIPT, { timeout: 1e4 });
|
|
4988
|
+
return normalizeSocketPathLines(output.split(/\r?\n/));
|
|
4904
4989
|
}
|
|
4905
4990
|
async function discoverRemoteDockerSocketPaths(opts) {
|
|
4906
4991
|
if (opts.executor) {
|
|
@@ -4923,7 +5008,13 @@ async function resolveRemoteDockerSocketPath(opts) {
|
|
|
4923
5008
|
if (cachedPath) {
|
|
4924
5009
|
return cachedPath;
|
|
4925
5010
|
}
|
|
4926
|
-
const pendingPath = discoverRemoteDockerSocketPaths(opts).then(
|
|
5011
|
+
const pendingPath = discoverRemoteDockerSocketPaths(opts).then(
|
|
5012
|
+
(paths) => paths[0] ?? DEFAULT_REMOTE_DOCKER_SOCKET_PATH,
|
|
5013
|
+
() => {
|
|
5014
|
+
resolvedDockerSocketPathCache.delete(opts);
|
|
5015
|
+
return DEFAULT_REMOTE_DOCKER_SOCKET_PATH;
|
|
5016
|
+
}
|
|
5017
|
+
);
|
|
4927
5018
|
resolvedDockerSocketPathCache.set(opts, pendingPath);
|
|
4928
5019
|
return pendingPath;
|
|
4929
5020
|
}
|
|
@@ -4943,35 +5034,98 @@ async function openStreamlocalUpstream(opts) {
|
|
|
4943
5034
|
throw error;
|
|
4944
5035
|
}
|
|
4945
5036
|
}
|
|
4946
|
-
function
|
|
5037
|
+
function respondBadGateway(client, reason) {
|
|
5038
|
+
if (client.destroyed || client.writableEnded) {
|
|
5039
|
+
client.destroy();
|
|
5040
|
+
return;
|
|
5041
|
+
}
|
|
5042
|
+
const body = JSON.stringify({ message: reason });
|
|
5043
|
+
try {
|
|
5044
|
+
client.end(
|
|
5045
|
+
`HTTP/1.1 502 Bad Gateway\r
|
|
5046
|
+
Content-Type: application/json\r
|
|
5047
|
+
Content-Length: ${Buffer.byteLength(body)}\r
|
|
5048
|
+
Connection: close\r
|
|
5049
|
+
\r
|
|
5050
|
+
` + body
|
|
5051
|
+
);
|
|
5052
|
+
} catch {
|
|
5053
|
+
client.destroy();
|
|
5054
|
+
}
|
|
5055
|
+
}
|
|
5056
|
+
function pipeThrough(client, upstream, handoff) {
|
|
5057
|
+
let answered = handoff.answered;
|
|
4947
5058
|
const teardown = () => {
|
|
4948
5059
|
client.destroy();
|
|
4949
5060
|
upstream.destroy();
|
|
4950
5061
|
};
|
|
4951
5062
|
client.on("error", teardown);
|
|
4952
|
-
upstream.on("error",
|
|
5063
|
+
upstream.on("error", () => {
|
|
5064
|
+
if (!answered) respondBadGateway(client, handoff.unansweredReason());
|
|
5065
|
+
upstream.destroy();
|
|
5066
|
+
});
|
|
4953
5067
|
client.once("close", () => upstream.destroy());
|
|
4954
|
-
upstream.once("close", () =>
|
|
5068
|
+
upstream.once("close", () => {
|
|
5069
|
+
if (!answered) {
|
|
5070
|
+
respondBadGateway(client, handoff.unansweredReason());
|
|
5071
|
+
return;
|
|
5072
|
+
}
|
|
5073
|
+
if (!client.destroyed) client.end();
|
|
5074
|
+
});
|
|
5075
|
+
upstream.on("data", () => {
|
|
5076
|
+
answered = true;
|
|
5077
|
+
});
|
|
4955
5078
|
client.pipe(upstream);
|
|
4956
|
-
upstream.pipe(client);
|
|
5079
|
+
upstream.pipe(client, { end: false });
|
|
5080
|
+
upstream.once("end", () => {
|
|
5081
|
+
if (answered && !client.writableEnded) client.end();
|
|
5082
|
+
});
|
|
4957
5083
|
}
|
|
4958
5084
|
function awaitFirstByte(upstream, ms) {
|
|
4959
5085
|
return new Promise((resolve) => {
|
|
4960
|
-
const finish = (
|
|
5086
|
+
const finish = (outcome) => {
|
|
4961
5087
|
clearTimeout(timer);
|
|
4962
5088
|
upstream.removeListener("data", onData);
|
|
4963
5089
|
upstream.removeListener("error", onFail);
|
|
4964
5090
|
upstream.removeListener("close", onFail);
|
|
4965
|
-
resolve(
|
|
5091
|
+
resolve(outcome);
|
|
4966
5092
|
};
|
|
4967
|
-
const onData = (chunk) => finish(chunk);
|
|
4968
|
-
const onFail = () => finish(
|
|
4969
|
-
const timer = setTimeout(() => finish(
|
|
5093
|
+
const onData = (chunk) => finish({ kind: "data", chunk });
|
|
5094
|
+
const onFail = () => finish({ kind: "closed" });
|
|
5095
|
+
const timer = setTimeout(() => finish({ kind: "silent" }), ms);
|
|
4970
5096
|
upstream.on("data", onData);
|
|
4971
5097
|
upstream.once("error", onFail);
|
|
4972
5098
|
upstream.once("close", onFail);
|
|
4973
5099
|
});
|
|
4974
5100
|
}
|
|
5101
|
+
function looksLikeHttpResponse(chunk) {
|
|
5102
|
+
const marker = "HTTP/";
|
|
5103
|
+
const head = chunk.subarray(0, marker.length).toString("latin1");
|
|
5104
|
+
return marker.startsWith(head) || head.startsWith(marker);
|
|
5105
|
+
}
|
|
5106
|
+
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.";
|
|
5107
|
+
function sentence(text) {
|
|
5108
|
+
const trimmed = text.trim();
|
|
5109
|
+
return /[.!?]$/.test(trimmed) ? trimmed : `${trimmed}.`;
|
|
5110
|
+
}
|
|
5111
|
+
function bridgeFailureReason(opts, cause) {
|
|
5112
|
+
const host = opts.host?.trim() || "the remote host";
|
|
5113
|
+
return `Could not reach the Docker daemon on ${host} over SSH: ${sentence(cause)} ${BRIDGE_FAILURE_HINT}`;
|
|
5114
|
+
}
|
|
5115
|
+
function upstreamClosedReason(opts, upstream, transport) {
|
|
5116
|
+
return bridgeFailureReason(
|
|
5117
|
+
opts,
|
|
5118
|
+
dialStdioDiagnostics(upstream) || `the ${transport} channel closed before answering the request`
|
|
5119
|
+
);
|
|
5120
|
+
}
|
|
5121
|
+
var REPLAY_SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
5122
|
+
function requestIsReplaySafe(chunks) {
|
|
5123
|
+
const head = chunks.find((c) => c.length > 0);
|
|
5124
|
+
if (!head) return true;
|
|
5125
|
+
const space = head.indexOf(32);
|
|
5126
|
+
if (space <= 0) return false;
|
|
5127
|
+
return REPLAY_SAFE_METHODS.has(head.subarray(0, space).toString("latin1"));
|
|
5128
|
+
}
|
|
4975
5129
|
function captureClient(client) {
|
|
4976
5130
|
const chunks = [];
|
|
4977
5131
|
let bytes = 0;
|
|
@@ -5006,9 +5160,15 @@ function captureClient(client) {
|
|
|
5006
5160
|
get overflowed() {
|
|
5007
5161
|
return overflowed;
|
|
5008
5162
|
},
|
|
5163
|
+
/** Whether this request may be handed to a second upstream — see
|
|
5164
|
+
* `requestIsReplaySafe`. Read at the moment of the decision, not at capture time. */
|
|
5165
|
+
get replaySafe() {
|
|
5166
|
+
return requestIsReplaySafe(chunks);
|
|
5167
|
+
},
|
|
5009
5168
|
/** Replay everything buffered so far onto `upstream` and forward the rest
|
|
5010
|
-
* live.
|
|
5011
|
-
* buffer is
|
|
5169
|
+
* live. Calling it again on a different upstream (the fallback) replays the
|
|
5170
|
+
* whole buffer — complete, because nothing is dropped until `commit`, but a
|
|
5171
|
+
* second dispatch to the daemon: gate that on `replaySafe`. */
|
|
5012
5172
|
feed(upstream) {
|
|
5013
5173
|
for (const chunk of chunks) upstream.write(chunk);
|
|
5014
5174
|
live = upstream;
|
|
@@ -5019,25 +5179,30 @@ function captureClient(client) {
|
|
|
5019
5179
|
live = null;
|
|
5020
5180
|
},
|
|
5021
5181
|
/** 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
|
-
|
|
5182
|
+
* bidi pipe, or destroy it if the client already went away. `handoff` tells the
|
|
5183
|
+
* pipe whether a response byte has already been forwarded, and what to say if
|
|
5184
|
+
* the upstream closes without ever producing one. */
|
|
5185
|
+
commit(upstream, handoff) {
|
|
5024
5186
|
detach();
|
|
5025
5187
|
chunks.length = 0;
|
|
5026
5188
|
if (closed) {
|
|
5027
5189
|
upstream.destroy();
|
|
5028
5190
|
return;
|
|
5029
5191
|
}
|
|
5030
|
-
pipeThrough(client, upstream);
|
|
5192
|
+
pipeThrough(client, upstream, handoff);
|
|
5031
5193
|
},
|
|
5032
|
-
/** Terminal: no usable upstream.
|
|
5033
|
-
*
|
|
5034
|
-
*
|
|
5035
|
-
*
|
|
5036
|
-
*
|
|
5037
|
-
*
|
|
5038
|
-
|
|
5194
|
+
/** Terminal: no usable upstream. Answer the request with `reason` (a real HTTP
|
|
5195
|
+
* 502 — see `respondBadGateway`) instead of destroying the socket, which is
|
|
5196
|
+
* what turned every one of these into `socket hang up`.
|
|
5197
|
+
*
|
|
5198
|
+
* Written BEFORE detach so the socket still has an error listener during the
|
|
5199
|
+
* write, and never with `destroy(err)`: a server-side socket cannot transmit a
|
|
5200
|
+
* JS Error to its peer, and destroy(err) emits a local 'error' event that —
|
|
5201
|
+
* once detach() has removed this socket's listener — would be unhandled and
|
|
5202
|
+
* take down the process over one dead Docker request. */
|
|
5203
|
+
fail(reason) {
|
|
5204
|
+
respondBadGateway(client, reason);
|
|
5039
5205
|
detach();
|
|
5040
|
-
client.destroy();
|
|
5041
5206
|
}
|
|
5042
5207
|
};
|
|
5043
5208
|
}
|
|
@@ -5049,12 +5214,7 @@ function createDockerSshBridge(opts) {
|
|
|
5049
5214
|
let pooledUnreliable = false;
|
|
5050
5215
|
const openDialStdioUpstream = async (forceEphemeral = false) => {
|
|
5051
5216
|
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;
|
|
5217
|
+
return attachDialStdioDiagnostics(await opts.executor.openDockerDialStdio());
|
|
5058
5218
|
}
|
|
5059
5219
|
if (!dialClient) {
|
|
5060
5220
|
const client = await connectSshClient(toSshConfig(opts));
|
|
@@ -5063,7 +5223,34 @@ function createDockerSshBridge(opts) {
|
|
|
5063
5223
|
});
|
|
5064
5224
|
dialClient = client;
|
|
5065
5225
|
}
|
|
5066
|
-
return
|
|
5226
|
+
return openDockerDialStdioChannel(dialClient);
|
|
5227
|
+
};
|
|
5228
|
+
const serveDialStdio = async (capture, client, upstream) => {
|
|
5229
|
+
capture.feed(upstream);
|
|
5230
|
+
const first = capture.overflowed ? { kind: "silent" } : await awaitFirstByte(upstream, DIAL_STDIO_ANSWER_TIMEOUT_MS);
|
|
5231
|
+
if (first.kind === "closed") {
|
|
5232
|
+
capture.stop();
|
|
5233
|
+
upstream.destroy();
|
|
5234
|
+
capture.fail(upstreamClosedReason(opts, upstream, DOCKER_DIAL_STDIO_COMMAND));
|
|
5235
|
+
return;
|
|
5236
|
+
}
|
|
5237
|
+
if (first.kind === "data" && !looksLikeHttpResponse(first.chunk)) {
|
|
5238
|
+
capture.stop();
|
|
5239
|
+
upstream.destroy();
|
|
5240
|
+
const banner = first.chunk.toString("utf8").trim().replace(/\s+/g, " ").slice(0, 300);
|
|
5241
|
+
capture.fail(
|
|
5242
|
+
bridgeFailureReason(
|
|
5243
|
+
opts,
|
|
5244
|
+
`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}\``
|
|
5245
|
+
)
|
|
5246
|
+
);
|
|
5247
|
+
return;
|
|
5248
|
+
}
|
|
5249
|
+
if (first.kind === "data") client.write(first.chunk);
|
|
5250
|
+
capture.commit(upstream, {
|
|
5251
|
+
answered: first.kind === "data",
|
|
5252
|
+
unansweredReason: () => upstreamClosedReason(opts, upstream, DOCKER_DIAL_STDIO_COMMAND)
|
|
5253
|
+
});
|
|
5067
5254
|
};
|
|
5068
5255
|
const decideMode = async () => {
|
|
5069
5256
|
let probe = null;
|
|
@@ -5111,24 +5298,20 @@ function createDockerSshBridge(opts) {
|
|
|
5111
5298
|
modeDecision ??= decideMode().then((mode) => upstreamMode = mode);
|
|
5112
5299
|
return modeDecision;
|
|
5113
5300
|
};
|
|
5114
|
-
const downgradeToDialStdio = async (capture, reason) => {
|
|
5301
|
+
const downgradeToDialStdio = async (capture, client, reason) => {
|
|
5115
5302
|
console.warn(
|
|
5116
5303
|
`[docker-ssh] streamlocal unusable (${opts.host ?? "?"}): ${reason} \u2014 downgrading this bridge to dial-stdio (fresh connection) and replaying the buffered request.`
|
|
5117
5304
|
);
|
|
5118
5305
|
upstreamMode = "dialstdio";
|
|
5119
5306
|
pooledUnreliable = true;
|
|
5120
|
-
|
|
5121
|
-
capture.feed(upstream);
|
|
5122
|
-
capture.commit(upstream);
|
|
5307
|
+
await serveDialStdio(capture, client, await openDialStdioUpstream(true));
|
|
5123
5308
|
};
|
|
5124
5309
|
const bridgeClient = async (client) => {
|
|
5125
5310
|
const capture = captureClient(client);
|
|
5126
5311
|
try {
|
|
5127
5312
|
const mode = await ensureMode();
|
|
5128
5313
|
if (mode === "dialstdio") {
|
|
5129
|
-
|
|
5130
|
-
capture.feed(upstream);
|
|
5131
|
-
capture.commit(upstream);
|
|
5314
|
+
await serveDialStdio(capture, client, await openDialStdioUpstream(pooledUnreliable));
|
|
5132
5315
|
return;
|
|
5133
5316
|
}
|
|
5134
5317
|
let channel;
|
|
@@ -5139,22 +5322,36 @@ function createDockerSshBridge(opts) {
|
|
|
5139
5322
|
`channel open timed out after ${STREAMLOCAL_DATA_VERIFY_TIMEOUT_MS / 1e3}s`
|
|
5140
5323
|
);
|
|
5141
5324
|
} catch (err) {
|
|
5142
|
-
await downgradeToDialStdio(capture, safeErrorMessage(err));
|
|
5325
|
+
await downgradeToDialStdio(capture, client, safeErrorMessage(err));
|
|
5143
5326
|
return;
|
|
5144
5327
|
}
|
|
5145
5328
|
capture.feed(channel);
|
|
5146
|
-
const firstByte = capture.overflowed ?
|
|
5147
|
-
if (capture.overflowed || firstByte) {
|
|
5148
|
-
if (firstByte) client.write(firstByte);
|
|
5149
|
-
capture.commit(channel
|
|
5329
|
+
const firstByte = capture.overflowed ? { kind: "silent" } : await awaitFirstByte(channel, STREAMLOCAL_DATA_VERIFY_TIMEOUT_MS);
|
|
5330
|
+
if (capture.overflowed || firstByte.kind === "data") {
|
|
5331
|
+
if (firstByte.kind === "data") client.write(firstByte.chunk);
|
|
5332
|
+
capture.commit(channel, {
|
|
5333
|
+
answered: firstByte.kind === "data",
|
|
5334
|
+
unansweredReason: () => upstreamClosedReason(opts, channel, "streamlocal")
|
|
5335
|
+
});
|
|
5336
|
+
return;
|
|
5337
|
+
}
|
|
5338
|
+
if (firstByte.kind === "silent" && !capture.replaySafe) {
|
|
5339
|
+
capture.commit(channel, {
|
|
5340
|
+
answered: false,
|
|
5341
|
+
unansweredReason: () => upstreamClosedReason(opts, channel, "streamlocal")
|
|
5342
|
+
});
|
|
5150
5343
|
return;
|
|
5151
5344
|
}
|
|
5152
5345
|
capture.stop();
|
|
5153
5346
|
channel.destroy();
|
|
5154
|
-
await downgradeToDialStdio(
|
|
5347
|
+
await downgradeToDialStdio(
|
|
5348
|
+
capture,
|
|
5349
|
+
client,
|
|
5350
|
+
firstByte.kind === "closed" ? "channel closed before answering" : "channel opened but no data flowed"
|
|
5351
|
+
);
|
|
5155
5352
|
} catch (err) {
|
|
5156
5353
|
console.warn(`[docker-ssh] bridge client failed (${opts.host ?? "?"}): ${safeErrorMessage(err)}`);
|
|
5157
|
-
capture.fail();
|
|
5354
|
+
capture.fail(bridgeFailureReason(opts, safeErrorMessage(err)));
|
|
5158
5355
|
}
|
|
5159
5356
|
};
|
|
5160
5357
|
const server = net2.createServer((client) => {
|
|
@@ -5201,7 +5398,7 @@ function createDockerSshBridge(opts) {
|
|
|
5201
5398
|
var DEFAULT_DOCKER_SOCKET_PATH = "/var/run/docker.sock";
|
|
5202
5399
|
var contextSocketCache = /* @__PURE__ */ new Map();
|
|
5203
5400
|
function resolveDockerContextSocket(env) {
|
|
5204
|
-
const configDir = env.DOCKER_CONFIG?.trim() ||
|
|
5401
|
+
const configDir = env.DOCKER_CONFIG?.trim() || join2(homedir2(), ".docker");
|
|
5205
5402
|
let name = env.DOCKER_CONTEXT?.trim();
|
|
5206
5403
|
const cacheKey = `${configDir}\0${name ?? ""}`;
|
|
5207
5404
|
const cached = contextSocketCache.get(cacheKey);
|
|
@@ -5209,13 +5406,13 @@ function resolveDockerContextSocket(env) {
|
|
|
5209
5406
|
let socket = null;
|
|
5210
5407
|
try {
|
|
5211
5408
|
if (!name) {
|
|
5212
|
-
const config = JSON.parse(readFileSync(
|
|
5409
|
+
const config = JSON.parse(readFileSync(join2(configDir, "config.json"), "utf8"));
|
|
5213
5410
|
name = typeof config.currentContext === "string" ? config.currentContext.trim() : "";
|
|
5214
5411
|
}
|
|
5215
5412
|
if (name && name !== "default") {
|
|
5216
5413
|
const digest = createHash("sha256").update(name).digest("hex");
|
|
5217
5414
|
const meta = JSON.parse(
|
|
5218
|
-
readFileSync(
|
|
5415
|
+
readFileSync(join2(configDir, "contexts", "meta", digest, "meta.json"), "utf8")
|
|
5219
5416
|
);
|
|
5220
5417
|
const host = meta.Endpoints?.docker?.Host;
|
|
5221
5418
|
if (typeof host === "string" && host.startsWith("unix://")) {
|
|
@@ -5330,6 +5527,35 @@ var RESTART_POLICIES = {
|
|
|
5330
5527
|
no: { Name: "no", MaximumRetryCount: 0 }
|
|
5331
5528
|
};
|
|
5332
5529
|
var DOCKER_BUILD_IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
5530
|
+
var activeDockerBuilds = /* @__PURE__ */ new Map();
|
|
5531
|
+
var pendingDockerBuildCancellations = /* @__PURE__ */ new Map();
|
|
5532
|
+
var PENDING_CANCEL_TTL_MS = 10 * 60 * 1e3;
|
|
5533
|
+
function cancelCovers(cancelledId, sessionId) {
|
|
5534
|
+
return sessionId === cancelledId || sessionId.startsWith(`${cancelledId}-`);
|
|
5535
|
+
}
|
|
5536
|
+
function recordPendingCancellation(sessionId) {
|
|
5537
|
+
clearTimeout(pendingDockerBuildCancellations.get(sessionId));
|
|
5538
|
+
const timer = setTimeout(
|
|
5539
|
+
() => pendingDockerBuildCancellations.delete(sessionId),
|
|
5540
|
+
PENDING_CANCEL_TTL_MS
|
|
5541
|
+
);
|
|
5542
|
+
timer.unref?.();
|
|
5543
|
+
pendingDockerBuildCancellations.set(sessionId, timer);
|
|
5544
|
+
}
|
|
5545
|
+
function registerDockerBuild(sessionId) {
|
|
5546
|
+
const abort = new AbortController();
|
|
5547
|
+
for (const cancelledId of pendingDockerBuildCancellations.keys()) {
|
|
5548
|
+
if (cancelCovers(cancelledId, sessionId)) {
|
|
5549
|
+
abort.abort();
|
|
5550
|
+
break;
|
|
5551
|
+
}
|
|
5552
|
+
}
|
|
5553
|
+
activeDockerBuilds.set(sessionId, abort);
|
|
5554
|
+
return abort;
|
|
5555
|
+
}
|
|
5556
|
+
function releaseDockerBuild(sessionId, abort) {
|
|
5557
|
+
if (activeDockerBuilds.get(sessionId) === abort) activeDockerBuilds.delete(sessionId);
|
|
5558
|
+
}
|
|
5333
5559
|
var DEFAULT_IN_CONTAINER_EXEC_TIMEOUT_MS = 12e4;
|
|
5334
5560
|
var SIGTERM_EXIT_CODE = 143;
|
|
5335
5561
|
var IN_CONTAINER_EXEC_WATCHDOG = [
|
|
@@ -5475,7 +5701,7 @@ function parsePortBindings(portSpecs) {
|
|
|
5475
5701
|
}
|
|
5476
5702
|
return { exposedPorts, portBindings };
|
|
5477
5703
|
}
|
|
5478
|
-
function packBuildContext(contextDir, entries) {
|
|
5704
|
+
function packBuildContext(contextDir, entries, cancelSignal) {
|
|
5479
5705
|
const controller = new AbortController();
|
|
5480
5706
|
const pack2 = tarFs.pack(contextDir, { entries });
|
|
5481
5707
|
const body = pack2.pipe(createGzip());
|
|
@@ -5484,6 +5710,10 @@ function packBuildContext(contextDir, entries) {
|
|
|
5484
5710
|
contextError ??= err instanceof Error ? err : new Error(String(err));
|
|
5485
5711
|
if (!controller.signal.aborted) controller.abort();
|
|
5486
5712
|
};
|
|
5713
|
+
if (cancelSignal) {
|
|
5714
|
+
if (cancelSignal.aborted) controller.abort();
|
|
5715
|
+
else cancelSignal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
5716
|
+
}
|
|
5487
5717
|
pack2.once("error", capture);
|
|
5488
5718
|
body.once("error", capture);
|
|
5489
5719
|
return { body, abortSignal: controller.signal, takeError: () => contextError };
|
|
@@ -5519,7 +5749,9 @@ function toDockerHealthcheck(hc) {
|
|
|
5519
5749
|
if (typeof hc.test === "string" && hc.test.trim()) {
|
|
5520
5750
|
Test = ["CMD-SHELL", hc.test];
|
|
5521
5751
|
} else if (Array.isArray(hc.test) && hc.test.length > 0) {
|
|
5522
|
-
|
|
5752
|
+
const head = hc.test[0];
|
|
5753
|
+
if (head === "NONE") return { Test: ["NONE"] };
|
|
5754
|
+
Test = head === "CMD" || head === "CMD-SHELL" ? [...hc.test] : ["CMD", ...hc.test];
|
|
5523
5755
|
}
|
|
5524
5756
|
if (!Test) return void 0;
|
|
5525
5757
|
const Interval = parseDurationNs(hc.interval);
|
|
@@ -5533,6 +5765,28 @@ function toDockerHealthcheck(hc) {
|
|
|
5533
5765
|
...StartPeriod !== void 0 && { StartPeriod }
|
|
5534
5766
|
};
|
|
5535
5767
|
}
|
|
5768
|
+
function toStopConfig(advanced) {
|
|
5769
|
+
const out = {};
|
|
5770
|
+
const signal = advanced?.stopSignal?.trim();
|
|
5771
|
+
if (signal) out.StopSignal = signal;
|
|
5772
|
+
const graceNs = parseDurationNs(advanced?.stopGracePeriod);
|
|
5773
|
+
if (graceNs !== void 0) {
|
|
5774
|
+
out.StopTimeout = graceNs <= 0 ? 0 : Math.max(1, Math.round(graceNs / 1e9));
|
|
5775
|
+
}
|
|
5776
|
+
return out;
|
|
5777
|
+
}
|
|
5778
|
+
async function gracefulStopForGrace(container) {
|
|
5779
|
+
let stopTimeout;
|
|
5780
|
+
try {
|
|
5781
|
+
stopTimeout = (await container.inspect()).Config.StopTimeout;
|
|
5782
|
+
} catch {
|
|
5783
|
+
return;
|
|
5784
|
+
}
|
|
5785
|
+
if (typeof stopTimeout === "number" && stopTimeout > 0) {
|
|
5786
|
+
await container.stop().catch(() => {
|
|
5787
|
+
});
|
|
5788
|
+
}
|
|
5789
|
+
}
|
|
5536
5790
|
function hasDockerFrameHeader(buf, offset = 0) {
|
|
5537
5791
|
return buf.length >= offset + 8 && (buf[offset] === 1 || buf[offset] === 2) && buf[offset + 1] === 0 && buf[offset + 2] === 0 && buf[offset + 3] === 0;
|
|
5538
5792
|
}
|
|
@@ -5654,7 +5908,15 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
5654
5908
|
"deploymentContainerQuery",
|
|
5655
5909
|
"hostContainerQuery",
|
|
5656
5910
|
"stabilityProbe",
|
|
5657
|
-
"containerEvents"
|
|
5911
|
+
"containerEvents",
|
|
5912
|
+
// Docker implements `inContainerExecutor` but had never DECLARED the capability.
|
|
5913
|
+
// Nothing gated on it (every caller tested for the method), so the omission was
|
|
5914
|
+
// invisible — but it meant the honest answer to supports("inContainerExec") was
|
|
5915
|
+
// wrong for the one runtime that best supports it.
|
|
5916
|
+
"inContainerExec",
|
|
5917
|
+
// A docker exec lands in the container's own namespaces, so a command run
|
|
5918
|
+
// through it cannot reach the host. Bare deliberately does NOT declare this.
|
|
5919
|
+
"isolatedExec"
|
|
5658
5920
|
]);
|
|
5659
5921
|
/** Docker honors every extended compose key we currently support. */
|
|
5660
5922
|
unsupportedComposeKeys = /* @__PURE__ */ new Set();
|
|
@@ -5752,7 +6014,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
5752
6014
|
*/
|
|
5753
6015
|
async estimateContextSize(dir) {
|
|
5754
6016
|
const { stat, readdir } = await import("fs/promises");
|
|
5755
|
-
const { join:
|
|
6017
|
+
const { join: join3 } = await import("path");
|
|
5756
6018
|
let total = 0;
|
|
5757
6019
|
const stack = [dir];
|
|
5758
6020
|
while (stack.length) {
|
|
@@ -5764,7 +6026,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
5764
6026
|
continue;
|
|
5765
6027
|
}
|
|
5766
6028
|
for (const entry of entries) {
|
|
5767
|
-
const full =
|
|
6029
|
+
const full = join3(current, entry.name);
|
|
5768
6030
|
if (entry.isDirectory()) {
|
|
5769
6031
|
stack.push(full);
|
|
5770
6032
|
} else if (entry.isFile()) {
|
|
@@ -5901,7 +6163,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
5901
6163
|
* context dir. One image; `dockerfileName` selects which Dockerfile in the
|
|
5902
6164
|
* shared tree to use (so N services can build from one transferred context).
|
|
5903
6165
|
*/
|
|
5904
|
-
async buildImageOnRemote(config, remoteContextDir, dockerfileName, tag, log) {
|
|
6166
|
+
async buildImageOnRemote(config, remoteContextDir, dockerfileName, tag, log, signal) {
|
|
5905
6167
|
const executor = this.connectionOptions?.executor;
|
|
5906
6168
|
if (!executor) throw new Error("SSH build path requires an executor on connectionOptions");
|
|
5907
6169
|
const buildArgs = Object.entries({
|
|
@@ -5916,10 +6178,15 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
5916
6178
|
log.log(`Running on remote: ${buildCmd}`);
|
|
5917
6179
|
log.log("\u2500\u2500\u2500 docker build output \u2500\u2500\u2500");
|
|
5918
6180
|
this.emitDockerStep(log, "install", "running", "Running install inside container (docker build)");
|
|
5919
|
-
const { code } = await executor.streamExec(
|
|
5920
|
-
|
|
5921
|
-
|
|
6181
|
+
const { code } = await executor.streamExec(
|
|
6182
|
+
buildCmd,
|
|
6183
|
+
(entry) => {
|
|
6184
|
+
log.log(entry.message, parseLogLevel(entry.message));
|
|
6185
|
+
},
|
|
6186
|
+
{ signal }
|
|
6187
|
+
);
|
|
5922
6188
|
log.log("\u2500\u2500\u2500 end docker build output \u2500\u2500\u2500");
|
|
6189
|
+
if (signal?.aborted) throw new BuildCancelledError();
|
|
5923
6190
|
if (code !== 0) throw new Error(`docker build exited with code ${code}`);
|
|
5924
6191
|
this.emitDockerStep(log, "install", "completed", "Image build finished");
|
|
5925
6192
|
}
|
|
@@ -6038,11 +6305,18 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6038
6305
|
await executor.writeFile(`${remoteContextDir}/${generatedName}`, generateDockerfile(config));
|
|
6039
6306
|
return generatedName;
|
|
6040
6307
|
}
|
|
6041
|
-
async buildViaSshTarPipe(config, buildContext, tag, log) {
|
|
6308
|
+
async buildViaSshTarPipe(config, buildContext, tag, log, signal) {
|
|
6042
6309
|
const remoteContextDir = `/tmp/openship-build-${config.sessionId}`;
|
|
6043
6310
|
try {
|
|
6044
6311
|
await this.transferBuildContext(buildContext.contextDir, remoteContextDir, log);
|
|
6045
|
-
await this.buildImageOnRemote(
|
|
6312
|
+
await this.buildImageOnRemote(
|
|
6313
|
+
config,
|
|
6314
|
+
remoteContextDir,
|
|
6315
|
+
buildContext.dockerfileName,
|
|
6316
|
+
tag,
|
|
6317
|
+
log,
|
|
6318
|
+
signal
|
|
6319
|
+
);
|
|
6046
6320
|
} finally {
|
|
6047
6321
|
this.connectionOptions?.executor?.exec(`rm -rf ${sq(remoteContextDir)}`).catch(() => {
|
|
6048
6322
|
});
|
|
@@ -6058,11 +6332,12 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6058
6332
|
* but it's correct for local/TCP where there's no separate SSH
|
|
6059
6333
|
* connection to piggyback on.
|
|
6060
6334
|
*/
|
|
6061
|
-
async buildViaDockerode(config, buildContext, tag, log) {
|
|
6335
|
+
async buildViaDockerode(config, buildContext, tag, log, cancelSignal) {
|
|
6062
6336
|
log.log(`Streaming build context to Docker daemon - image tag: ${tag}`);
|
|
6063
6337
|
const { body, abortSignal, takeError } = packBuildContext(
|
|
6064
6338
|
buildContext.contextDir,
|
|
6065
|
-
buildContext.contextEntries
|
|
6339
|
+
buildContext.contextEntries,
|
|
6340
|
+
cancelSignal
|
|
6066
6341
|
);
|
|
6067
6342
|
try {
|
|
6068
6343
|
const stream = await this.docker.buildImage(body, {
|
|
@@ -6195,7 +6470,17 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6195
6470
|
const log = logger ?? new BuildLogger();
|
|
6196
6471
|
const startTime = Date.now();
|
|
6197
6472
|
const tag = this.imageTag(config.slug, config.sessionId);
|
|
6473
|
+
const abort = registerDockerBuild(config.sessionId);
|
|
6474
|
+
const cancelled = () => {
|
|
6475
|
+
log.step("build", "failed", "Docker build cancelled");
|
|
6476
|
+
return {
|
|
6477
|
+
sessionId: config.sessionId,
|
|
6478
|
+
status: "cancelled",
|
|
6479
|
+
durationMs: Date.now() - startTime
|
|
6480
|
+
};
|
|
6481
|
+
};
|
|
6198
6482
|
try {
|
|
6483
|
+
if (abort.signal.aborted) return cancelled();
|
|
6199
6484
|
log.log(`Build strategy: docker (${this.transport.description})
|
|
6200
6485
|
`);
|
|
6201
6486
|
try {
|
|
@@ -6209,6 +6494,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6209
6494
|
try {
|
|
6210
6495
|
this.emitDockerStep(log, "clone", "running", "Cloning source on the server...");
|
|
6211
6496
|
await this.cloneSourceOnRemote(config, remoteContextDir, log);
|
|
6497
|
+
if (abort.signal.aborted) throw new BuildCancelledError();
|
|
6212
6498
|
this.emitDockerStep(log, "clone", "completed", "Source cloned on the server");
|
|
6213
6499
|
const dockerfileName = await this.resolveRemoteDockerfile(
|
|
6214
6500
|
config,
|
|
@@ -6216,11 +6502,19 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6216
6502
|
"Dockerfile.openship",
|
|
6217
6503
|
config.stack === "docker"
|
|
6218
6504
|
);
|
|
6219
|
-
await this.buildImageOnRemote(
|
|
6505
|
+
await this.buildImageOnRemote(
|
|
6506
|
+
config,
|
|
6507
|
+
remoteContextDir,
|
|
6508
|
+
dockerfileName,
|
|
6509
|
+
tag,
|
|
6510
|
+
log,
|
|
6511
|
+
abort.signal
|
|
6512
|
+
);
|
|
6220
6513
|
} finally {
|
|
6221
6514
|
sshExecutor.exec(`rm -rf ${sq(remoteContextDir)}`).catch(() => {
|
|
6222
6515
|
});
|
|
6223
6516
|
}
|
|
6517
|
+
if (abort.signal.aborted) return cancelled();
|
|
6224
6518
|
await this.verifyImageBuilt(tag);
|
|
6225
6519
|
log.log(`Image ${tag} is ready.
|
|
6226
6520
|
`);
|
|
@@ -6232,6 +6526,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6232
6526
|
requireRepositoryDockerfile: config.stack === "docker",
|
|
6233
6527
|
onLog: log.callback
|
|
6234
6528
|
});
|
|
6529
|
+
if (abort.signal.aborted) throw new BuildCancelledError();
|
|
6235
6530
|
try {
|
|
6236
6531
|
const sizeBytes = await this.estimateContextSize(buildContext.contextDir);
|
|
6237
6532
|
const sizeMB = (sizeBytes / 1024 / 1024).toFixed(1);
|
|
@@ -6268,10 +6563,11 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6268
6563
|
this.emitDockerStep(log, "build", "skipped", "No build command configured");
|
|
6269
6564
|
}
|
|
6270
6565
|
if (sshExecutor) {
|
|
6271
|
-
await this.buildViaSshTarPipe(config, buildContext, tag, log);
|
|
6566
|
+
await this.buildViaSshTarPipe(config, buildContext, tag, log, abort.signal);
|
|
6272
6567
|
} else {
|
|
6273
|
-
await this.buildViaDockerode(config, buildContext, tag, log);
|
|
6568
|
+
await this.buildViaDockerode(config, buildContext, tag, log, abort.signal);
|
|
6274
6569
|
}
|
|
6570
|
+
if (abort.signal.aborted) return cancelled();
|
|
6275
6571
|
await this.verifyImageBuilt(tag);
|
|
6276
6572
|
log.log(`Image ${tag} is ready.
|
|
6277
6573
|
`);
|
|
@@ -6280,9 +6576,12 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6280
6576
|
const durationMs = Date.now() - startTime;
|
|
6281
6577
|
return { sessionId: config.sessionId, status: "deploying", imageRef: tag, durationMs };
|
|
6282
6578
|
} catch (err) {
|
|
6579
|
+
if (abort.signal.aborted || err instanceof BuildCancelledError) return cancelled();
|
|
6283
6580
|
const msg = safeErrorMessage(err);
|
|
6284
6581
|
log.step("build", "failed", `Docker build failed: ${msg}`);
|
|
6285
6582
|
return { sessionId: config.sessionId, status: "failed", durationMs: Date.now() - startTime, errorMessage: `Docker build failed: ${msg}` };
|
|
6583
|
+
} finally {
|
|
6584
|
+
releaseDockerBuild(config.sessionId, abort);
|
|
6286
6585
|
}
|
|
6287
6586
|
}
|
|
6288
6587
|
/**
|
|
@@ -6344,23 +6643,33 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6344
6643
|
}
|
|
6345
6644
|
const tag = buildResult.imageRef;
|
|
6346
6645
|
const extractStart = Date.now();
|
|
6646
|
+
const abort = registerDockerBuild(config.sessionId);
|
|
6647
|
+
const elapsed = () => (buildResult.durationMs ?? 0) + (Date.now() - extractStart);
|
|
6648
|
+
const cancelled = () => {
|
|
6649
|
+
log.step("build", "failed", "Static extract cancelled");
|
|
6650
|
+
return { sessionId: config.sessionId, status: "cancelled", durationMs: elapsed() };
|
|
6651
|
+
};
|
|
6347
6652
|
try {
|
|
6348
6653
|
await this.moveStaticBuildToHost(tag, config, hostOutDir, log);
|
|
6654
|
+
if (abort.signal.aborted) return cancelled();
|
|
6349
6655
|
return {
|
|
6350
6656
|
sessionId: config.sessionId,
|
|
6351
6657
|
status: "deploying",
|
|
6352
6658
|
imageRef: hostOutDir,
|
|
6353
|
-
durationMs: (
|
|
6659
|
+
durationMs: elapsed()
|
|
6354
6660
|
};
|
|
6355
6661
|
} catch (err) {
|
|
6662
|
+
if (abort.signal.aborted || err instanceof BuildCancelledError) return cancelled();
|
|
6356
6663
|
const msg = safeErrorMessage(err);
|
|
6357
6664
|
log.step("build", "failed", `Static extract failed: ${msg}`);
|
|
6358
6665
|
return {
|
|
6359
6666
|
sessionId: config.sessionId,
|
|
6360
6667
|
status: "failed",
|
|
6361
|
-
durationMs: (
|
|
6668
|
+
durationMs: elapsed(),
|
|
6362
6669
|
errorMessage: `Static extract failed: ${msg}`
|
|
6363
6670
|
};
|
|
6671
|
+
} finally {
|
|
6672
|
+
releaseDockerBuild(config.sessionId, abort);
|
|
6364
6673
|
}
|
|
6365
6674
|
}
|
|
6366
6675
|
/**
|
|
@@ -6394,6 +6703,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6394
6703
|
async extractDocRootOverSsh(sshExecutor, tag, docRoot, hostOutDir) {
|
|
6395
6704
|
const cid = (await sshExecutor.exec(`docker create ${sq(tag)}`)).trim();
|
|
6396
6705
|
try {
|
|
6706
|
+
await ensureOwnedDir(sshExecutor, dirOf(hostOutDir));
|
|
6397
6707
|
await sshExecutor.exec(`mkdir -p ${sq(hostOutDir)}`);
|
|
6398
6708
|
await sshExecutor.exec(`docker cp ${sq(`${cid}:${docRoot}/.`)} ${sq(hostOutDir)}`);
|
|
6399
6709
|
const listing = (await sshExecutor.exec(`ls -A ${sq(hostOutDir)} | head -1`).catch(() => "")).trim();
|
|
@@ -6434,12 +6744,12 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6434
6744
|
*/
|
|
6435
6745
|
async extractDocRootViaBindMount(tag, docRoot, hostOutDir) {
|
|
6436
6746
|
const { mkdir, readdir, writeFile, rm } = await import("fs/promises");
|
|
6437
|
-
const { join:
|
|
6747
|
+
const { join: join3 } = await import("path");
|
|
6438
6748
|
const { randomBytes } = await import("crypto");
|
|
6439
6749
|
await mkdir(hostOutDir, { recursive: true });
|
|
6440
6750
|
const OUT = "/__openship_out";
|
|
6441
6751
|
const probe = `.openship-extract-probe-${randomBytes(6).toString("hex")}`;
|
|
6442
|
-
await writeFile(
|
|
6752
|
+
await writeFile(join3(hostOutDir, probe), "probe");
|
|
6443
6753
|
const container = await this.docker.createContainer({
|
|
6444
6754
|
Image: tag,
|
|
6445
6755
|
// Override the ENTRYPOINT: builder base images (node, bun) ship a
|
|
@@ -6479,7 +6789,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6479
6789
|
} finally {
|
|
6480
6790
|
await container.remove({ force: true }).catch(() => {
|
|
6481
6791
|
});
|
|
6482
|
-
await rm(
|
|
6792
|
+
await rm(join3(hostOutDir, probe), { force: true }).catch(() => {
|
|
6483
6793
|
});
|
|
6484
6794
|
}
|
|
6485
6795
|
}
|
|
@@ -6542,16 +6852,34 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6542
6852
|
const isSsh = this.transport.kind === "ssh" && !!this.connectionOptions?.executor;
|
|
6543
6853
|
const cloneOnServer = isSsh && !!source.cloneOnServer;
|
|
6544
6854
|
const remoteContextDir = `/tmp/openship-build-${source.sessionId}`;
|
|
6855
|
+
const abortControllers = new Map(
|
|
6856
|
+
specs.map((spec) => [spec.config.sessionId, registerDockerBuild(spec.config.sessionId)])
|
|
6857
|
+
);
|
|
6858
|
+
const isCancelled = (sessionId) => abortControllers.get(sessionId)?.signal.aborted === true;
|
|
6859
|
+
const allCancelled = () => [...abortControllers.values()].every((c) => c.signal.aborted);
|
|
6860
|
+
const cancelledResult = (sessionId, startedAt) => ({
|
|
6861
|
+
sessionId,
|
|
6862
|
+
status: "cancelled",
|
|
6863
|
+
durationMs: Date.now() - startedAt
|
|
6864
|
+
});
|
|
6545
6865
|
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
6866
|
try {
|
|
6867
|
+
if (allCancelled()) {
|
|
6868
|
+
const startedAt = Date.now();
|
|
6869
|
+
return specs.map((spec) => {
|
|
6870
|
+
const result = cancelledResult(spec.config.sessionId, startedAt);
|
|
6871
|
+
spec.onResult?.(result);
|
|
6872
|
+
return { serviceName: spec.serviceName, result };
|
|
6873
|
+
});
|
|
6874
|
+
}
|
|
6875
|
+
if (cloneOnServer) {
|
|
6876
|
+
prepareLogger.step("clone", "running", "Cloning source on the server...");
|
|
6877
|
+
await this.cloneSourceOnRemote(source, remoteContextDir, prepareLogger);
|
|
6878
|
+
prepareLogger.step("clone", "completed", "Source cloned on the server");
|
|
6879
|
+
} else {
|
|
6880
|
+
prepareLogger.step("clone", "running", "Preparing shared build context...");
|
|
6881
|
+
tree = await prepareSourceTree(source, { onLog: prepareLogger.callback });
|
|
6882
|
+
}
|
|
6555
6883
|
const resolvedList = await Promise.all(
|
|
6556
6884
|
specs.map(async (spec) => {
|
|
6557
6885
|
const generatedName = `Dockerfile.openship.${spec.config.sessionId}`;
|
|
@@ -6602,7 +6930,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6602
6930
|
} catch {
|
|
6603
6931
|
prepareLogger.step("clone", "completed", "Shared build context ready");
|
|
6604
6932
|
}
|
|
6605
|
-
if (isSsh) {
|
|
6933
|
+
if (isSsh && !allCancelled()) {
|
|
6606
6934
|
await this.transferBuildContext(tree.contextDir, remoteContextDir, prepareLogger);
|
|
6607
6935
|
}
|
|
6608
6936
|
}
|
|
@@ -6610,6 +6938,13 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6610
6938
|
for (const { spec, dockerfileName, contextEntries, error } of resolvedList) {
|
|
6611
6939
|
const startedAt = Date.now();
|
|
6612
6940
|
const tag = this.imageTag(spec.config.slug, spec.config.sessionId);
|
|
6941
|
+
const signal = abortControllers.get(spec.config.sessionId)?.signal;
|
|
6942
|
+
if (isCancelled(spec.config.sessionId)) {
|
|
6943
|
+
const result = cancelledResult(spec.config.sessionId, startedAt);
|
|
6944
|
+
spec.onResult?.(result);
|
|
6945
|
+
results.push({ serviceName: spec.serviceName, result });
|
|
6946
|
+
continue;
|
|
6947
|
+
}
|
|
6613
6948
|
if (error || !dockerfileName) {
|
|
6614
6949
|
const result = {
|
|
6615
6950
|
sessionId: spec.config.sessionId,
|
|
@@ -6629,12 +6964,14 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6629
6964
|
remoteContextDir,
|
|
6630
6965
|
dockerfileName,
|
|
6631
6966
|
tag,
|
|
6632
|
-
spec.logger
|
|
6967
|
+
spec.logger,
|
|
6968
|
+
signal
|
|
6633
6969
|
);
|
|
6634
6970
|
} else {
|
|
6635
6971
|
const { body, abortSignal, takeError } = packBuildContext(
|
|
6636
6972
|
tree.contextDir,
|
|
6637
|
-
contextEntries ?? []
|
|
6973
|
+
contextEntries ?? [],
|
|
6974
|
+
signal
|
|
6638
6975
|
);
|
|
6639
6976
|
try {
|
|
6640
6977
|
const stream = await this.docker.buildImage(body, {
|
|
@@ -6655,6 +6992,12 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6655
6992
|
throw contextErr ? new Error(`Failed to read Docker build context: ${safeErrorMessage(contextErr)}`, { cause: contextErr }) : err;
|
|
6656
6993
|
}
|
|
6657
6994
|
}
|
|
6995
|
+
if (isCancelled(spec.config.sessionId)) {
|
|
6996
|
+
const result2 = cancelledResult(spec.config.sessionId, startedAt);
|
|
6997
|
+
spec.onResult?.(result2);
|
|
6998
|
+
results.push({ serviceName: spec.serviceName, result: result2 });
|
|
6999
|
+
continue;
|
|
7000
|
+
}
|
|
6658
7001
|
await this.verifyImageBuilt(tag);
|
|
6659
7002
|
if (spec.config.staticExtractOnly && spec.config.staticOutDir) {
|
|
6660
7003
|
await this.moveStaticBuildToHost(
|
|
@@ -6663,7 +7006,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6663
7006
|
spec.config.staticOutDir,
|
|
6664
7007
|
spec.logger
|
|
6665
7008
|
);
|
|
6666
|
-
const result2 = {
|
|
7009
|
+
const result2 = isCancelled(spec.config.sessionId) ? cancelledResult(spec.config.sessionId, startedAt) : {
|
|
6667
7010
|
sessionId: spec.config.sessionId,
|
|
6668
7011
|
status: "deploying",
|
|
6669
7012
|
imageRef: spec.config.staticOutDir,
|
|
@@ -6684,6 +7027,13 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6684
7027
|
spec.onResult?.(result);
|
|
6685
7028
|
results.push({ serviceName: spec.serviceName, result });
|
|
6686
7029
|
} catch (err) {
|
|
7030
|
+
if (isCancelled(spec.config.sessionId) || err instanceof BuildCancelledError) {
|
|
7031
|
+
spec.logger.log("Docker build cancelled\n", "error");
|
|
7032
|
+
const result2 = cancelledResult(spec.config.sessionId, startedAt);
|
|
7033
|
+
spec.onResult?.(result2);
|
|
7034
|
+
results.push({ serviceName: spec.serviceName, result: result2 });
|
|
7035
|
+
continue;
|
|
7036
|
+
}
|
|
6687
7037
|
const msg = safeErrorMessage(err);
|
|
6688
7038
|
spec.logger.log(`Docker build failed: ${msg}
|
|
6689
7039
|
`, "error");
|
|
@@ -6699,6 +7049,9 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6699
7049
|
}
|
|
6700
7050
|
return results;
|
|
6701
7051
|
} finally {
|
|
7052
|
+
for (const [sessionId, abort] of abortControllers) {
|
|
7053
|
+
releaseDockerBuild(sessionId, abort);
|
|
7054
|
+
}
|
|
6702
7055
|
if (isSsh) {
|
|
6703
7056
|
this.connectionOptions?.executor?.exec(`rm -rf ${sq(remoteContextDir)}`).catch(() => {
|
|
6704
7057
|
});
|
|
@@ -6707,11 +7060,23 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6707
7060
|
}
|
|
6708
7061
|
}
|
|
6709
7062
|
async cancelBuild(sessionId) {
|
|
7063
|
+
for (const [activeId, abort] of activeDockerBuilds) {
|
|
7064
|
+
if (cancelCovers(sessionId, activeId)) abort.abort();
|
|
7065
|
+
}
|
|
7066
|
+
recordPendingCancellation(sessionId);
|
|
7067
|
+
const executor = this.transport.kind === "ssh" ? this.connectionOptions?.executor : void 0;
|
|
7068
|
+
if (executor) {
|
|
7069
|
+
await killProcessesUnderDir(executor, `/tmp/openship-build-${sessionId}`, {
|
|
7070
|
+
includeSuffixed: true
|
|
7071
|
+
});
|
|
7072
|
+
}
|
|
6710
7073
|
const containers = await this.docker.listContainers({
|
|
6711
7074
|
all: true,
|
|
6712
|
-
filters: { label: [
|
|
7075
|
+
filters: { label: ["openship.build"] }
|
|
6713
7076
|
});
|
|
6714
7077
|
for (const c of containers) {
|
|
7078
|
+
const buildId = c.Labels?.["openship.build"];
|
|
7079
|
+
if (!buildId || !cancelCovers(sessionId, buildId)) continue;
|
|
6715
7080
|
try {
|
|
6716
7081
|
await this.docker.getContainer(c.Id).remove({ force: true });
|
|
6717
7082
|
} catch {
|
|
@@ -6732,7 +7097,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6732
7097
|
}
|
|
6733
7098
|
const containerName = `openship-${config.runtimeName || config.projectId}-${config.deploymentId}`;
|
|
6734
7099
|
const env = [
|
|
6735
|
-
`PORT=${config.port}
|
|
7100
|
+
...config.portless ? [] : [`PORT=${config.port}`],
|
|
6736
7101
|
`NODE_ENV=${config.environment === "production" ? "production" : "development"}`,
|
|
6737
7102
|
...Object.entries(config.envVars).map(([k, v]) => `${k}=${v}`)
|
|
6738
7103
|
];
|
|
@@ -6796,7 +7161,9 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6796
7161
|
// rebuild, exactly like a compose service.
|
|
6797
7162
|
...networkId ? { service: config.networkAlias } : {}
|
|
6798
7163
|
}),
|
|
6799
|
-
|
|
7164
|
+
// A worker exposes and publishes no port (#538-B); everything else exposes
|
|
7165
|
+
// its app port for the loopback publish below.
|
|
7166
|
+
...config.portless ? {} : { ExposedPorts: { [`${config.port}/tcp`]: {} } },
|
|
6800
7167
|
...networkId ? { NetworkingConfig: { EndpointsConfig: { [networkId]: { Aliases: aliases } } } } : {},
|
|
6801
7168
|
HostConfig: {
|
|
6802
7169
|
RestartPolicy: restartPolicy,
|
|
@@ -6814,10 +7181,13 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6814
7181
|
// app directly, bypassing the edge's SSL/rate-limit/rules (and Docker's
|
|
6815
7182
|
// iptables bypass ufw). A pinned `config.hostPort` (loopback-port route
|
|
6816
7183
|
// strategy) is stable across redeploys; otherwise a random loopback port.
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
|
|
6820
|
-
|
|
7184
|
+
// A worker (config.portless) binds no host port at all (#538-B).
|
|
7185
|
+
...config.portless ? {} : {
|
|
7186
|
+
PortBindings: {
|
|
7187
|
+
[`${config.port}/tcp`]: [
|
|
7188
|
+
{ HostIp: "127.0.0.1", HostPort: config.hostPort ? String(config.hostPort) : "" }
|
|
7189
|
+
]
|
|
7190
|
+
}
|
|
6821
7191
|
}
|
|
6822
7192
|
}
|
|
6823
7193
|
});
|
|
@@ -6869,6 +7239,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6869
7239
|
}
|
|
6870
7240
|
const container = this.docker.getContainer(containerId);
|
|
6871
7241
|
try {
|
|
7242
|
+
await gracefulStopForGrace(container);
|
|
6872
7243
|
await container.remove({ force: true });
|
|
6873
7244
|
} catch (err) {
|
|
6874
7245
|
if (!isDockerNotFoundError(err)) throw err;
|
|
@@ -7135,13 +7506,39 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7135
7506
|
}
|
|
7136
7507
|
const executor = this.connectionOptions?.executor;
|
|
7137
7508
|
if (executor) {
|
|
7138
|
-
|
|
7509
|
+
const timeout = 10 * 6e4;
|
|
7510
|
+
const auth = await this.connectionOptions?.resolveRegistryAuth?.(ref);
|
|
7511
|
+
const config = auth ? dockerConfigJsonFor(auth) : null;
|
|
7512
|
+
if (!config) {
|
|
7513
|
+
await executor.exec(`docker pull ${sq(ref)}`, { timeout });
|
|
7514
|
+
return;
|
|
7515
|
+
}
|
|
7516
|
+
const dir = `/tmp/openship-pull-${crypto.randomUUID()}`;
|
|
7517
|
+
try {
|
|
7518
|
+
await executor.exec(`mkdir -m 700 ${sq(dir)}`);
|
|
7519
|
+
await executor.writeFile(`${dir}/config.json`, config);
|
|
7520
|
+
await executor.exec(`chmod 600 ${sq(dir)}/config.json`);
|
|
7521
|
+
await executor.exec(
|
|
7522
|
+
`DOCKER_CONFIG=${sq(dir)} docker pull ${sq(ref)}; rc=$?; rm -rf ${sq(dir)}; exit $rc`,
|
|
7523
|
+
{ timeout }
|
|
7524
|
+
);
|
|
7525
|
+
} finally {
|
|
7526
|
+
await executor.exec(`rm -rf ${sq(dir)}`).catch(() => {
|
|
7527
|
+
});
|
|
7528
|
+
}
|
|
7139
7529
|
return;
|
|
7140
7530
|
}
|
|
7141
|
-
const
|
|
7142
|
-
|
|
7143
|
-
this.docker.
|
|
7144
|
-
|
|
7531
|
+
const authconfig = this.connectionOptions?.resolveRegistryAuth ? await this.connectionOptions.resolveRegistryAuth(ref) : await resolveDockerAuth(ref);
|
|
7532
|
+
try {
|
|
7533
|
+
const stream = await this.docker.pull(ref, authconfig ? { authconfig } : {});
|
|
7534
|
+
await new Promise((resolve, reject) => {
|
|
7535
|
+
this.docker.modem.followProgress(stream, (err) => err ? reject(err) : resolve());
|
|
7536
|
+
});
|
|
7537
|
+
} catch (err) {
|
|
7538
|
+
throw new Error(
|
|
7539
|
+
`Failed to pull ${ref} from ${registryForImage(ref)}: ${safeErrorMessage(err)}`
|
|
7540
|
+
);
|
|
7541
|
+
}
|
|
7145
7542
|
}
|
|
7146
7543
|
/** Is this image tag present on THIS daemon? Distinguishes a locally-built
|
|
7147
7544
|
* image (must be transferred cross-server) from a registry tag (the target
|
|
@@ -7731,16 +8128,17 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7731
8128
|
const network = this.docker.getNetwork(networkId);
|
|
7732
8129
|
for (const m of members) {
|
|
7733
8130
|
if (!m.containerId) continue;
|
|
8131
|
+
const aliases = m.aliases.filter(Boolean);
|
|
7734
8132
|
try {
|
|
7735
8133
|
await network.connect({
|
|
7736
8134
|
Container: m.containerId,
|
|
7737
|
-
EndpointConfig:
|
|
8135
|
+
EndpointConfig: aliases.length ? { Aliases: aliases } : {}
|
|
7738
8136
|
});
|
|
7739
8137
|
} catch (err) {
|
|
7740
8138
|
const msg = err?.message ?? "";
|
|
7741
8139
|
if (!/already exists|already connected/i.test(msg)) {
|
|
7742
8140
|
console.warn(
|
|
7743
|
-
`[docker] group join failed for ${m.containerId.slice(0, 12)} (${
|
|
8141
|
+
`[docker] group join failed for ${m.containerId.slice(0, 12)} (${aliases.join(", ")}): ${msg}`
|
|
7744
8142
|
);
|
|
7745
8143
|
}
|
|
7746
8144
|
}
|
|
@@ -7802,6 +8200,20 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7802
8200
|
* the narrow case where the network's identity changed out-of-band (docker
|
|
7803
8201
|
* network prune/rm, daemon/host rebuild) and surviving containers fell off it.
|
|
7804
8202
|
*/
|
|
8203
|
+
/**
|
|
8204
|
+
* A container that can never be attached to a network, because it doesn't own a
|
|
8205
|
+
* network namespace to attach (compose `network_mode: container:…`/`none`, or a
|
|
8206
|
+
* migration-adopted `host`).
|
|
8207
|
+
*
|
|
8208
|
+
* Both membership loops below are best-effort and swallow errors, so without this
|
|
8209
|
+
* they'd still work — they'd just log "connect failed" for such a container on
|
|
8210
|
+
* every single deploy, forever, over something that is correct by design. Read
|
|
8211
|
+
* from `HostConfig.NetworkMode`, which `/containers/json` reports.
|
|
8212
|
+
*/
|
|
8213
|
+
cannotJoinNetworks(container) {
|
|
8214
|
+
const mode = container.HostConfig?.NetworkMode;
|
|
8215
|
+
return !!mode && (mode.startsWith("container:") || mode === "none" || mode === "host");
|
|
8216
|
+
}
|
|
7805
8217
|
async reconcileNetworkMembership(networkId, projectId) {
|
|
7806
8218
|
let containers;
|
|
7807
8219
|
try {
|
|
@@ -7818,6 +8230,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7818
8230
|
(n) => n?.NetworkID === networkId
|
|
7819
8231
|
);
|
|
7820
8232
|
if (onNetwork) continue;
|
|
8233
|
+
if (this.cannotJoinNetworks(c)) continue;
|
|
7821
8234
|
const service = c.Labels?.["openship.service"];
|
|
7822
8235
|
try {
|
|
7823
8236
|
await network.connect({
|
|
@@ -7867,6 +8280,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7867
8280
|
(n) => n?.NetworkID === netId
|
|
7868
8281
|
);
|
|
7869
8282
|
if (onNetwork) continue;
|
|
8283
|
+
if (this.cannotJoinNetworks(c)) continue;
|
|
7870
8284
|
try {
|
|
7871
8285
|
await network.connect({ Container: c.Id });
|
|
7872
8286
|
} catch (err) {
|
|
@@ -7901,6 +8315,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7901
8315
|
const containerName = `openship-${config.slug}-${config.serviceName}`;
|
|
7902
8316
|
try {
|
|
7903
8317
|
const existing = this.docker.getContainer(containerName);
|
|
8318
|
+
await gracefulStopForGrace(existing);
|
|
7904
8319
|
await existing.remove({ force: true });
|
|
7905
8320
|
} catch {
|
|
7906
8321
|
}
|
|
@@ -7909,6 +8324,11 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7909
8324
|
...Object.entries(config.environment).map(([k, v]) => `${k}=${v}`)
|
|
7910
8325
|
];
|
|
7911
8326
|
const cmd = resolveComposeCmd(config);
|
|
8327
|
+
const entrypoint = resolveComposeEntrypoint(config.advanced);
|
|
8328
|
+
const sharedNetwork = config.namespaces?.network;
|
|
8329
|
+
const sharesNetns = !!sharedNetwork && sharedNetwork !== "none";
|
|
8330
|
+
const ownsProjectEndpoint = ownsNetworkEndpoint(sharedNetwork);
|
|
8331
|
+
const sharedPid = config.namespaces?.pid;
|
|
7912
8332
|
const { exposedPorts, portBindings } = parsePortBindings(config.ports);
|
|
7913
8333
|
if (!config.namespaceVolumes) {
|
|
7914
8334
|
await this.assertNoForeignNamedVolumeCollision(config);
|
|
@@ -7917,6 +8337,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7917
8337
|
const binds = scopedBinds.length > 0 ? scopedBinds : void 0;
|
|
7918
8338
|
const restartPolicy = resolveRestartPolicy(config.restart);
|
|
7919
8339
|
const healthcheck = toDockerHealthcheck(config.advanced?.healthcheck);
|
|
8340
|
+
const stopConfig = toStopConfig(config.advanced);
|
|
7920
8341
|
log({
|
|
7921
8342
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7922
8343
|
message: `Creating service container ${containerName} from ${config.image}...
|
|
@@ -7929,6 +8350,23 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7929
8350
|
`,
|
|
7930
8351
|
level: "info"
|
|
7931
8352
|
});
|
|
8353
|
+
if (sharedNetwork) {
|
|
8354
|
+
log({
|
|
8355
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8356
|
+
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.
|
|
8357
|
+
` : ` and no network access at all.
|
|
8358
|
+
`),
|
|
8359
|
+
level: "warn"
|
|
8360
|
+
});
|
|
8361
|
+
}
|
|
8362
|
+
if (sharedPid) {
|
|
8363
|
+
log({
|
|
8364
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8365
|
+
message: `PID namespace: ${sharedPid}
|
|
8366
|
+
`,
|
|
8367
|
+
level: "info"
|
|
8368
|
+
});
|
|
8369
|
+
}
|
|
7932
8370
|
if (!config.image.startsWith("openship/")) {
|
|
7933
8371
|
try {
|
|
7934
8372
|
log({
|
|
@@ -7952,8 +8390,16 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7952
8390
|
name: containerName,
|
|
7953
8391
|
Image: config.image,
|
|
7954
8392
|
Cmd: cmd,
|
|
8393
|
+
// Entrypoint (#575). SPREAD rather than assigned, because `undefined` and `[]`
|
|
8394
|
+
// must reach Docker differently: absent leaves the image's own ENTRYPOINT
|
|
8395
|
+
// alone, while `Entrypoint: []` clears it. Assigning `undefined` would send the
|
|
8396
|
+
// key, and the same conditional-spread shape is why `toStopConfig` returns an
|
|
8397
|
+
// object instead of nullable fields.
|
|
8398
|
+
...entrypoint === void 0 ? {} : { Entrypoint: entrypoint },
|
|
7955
8399
|
Env: env,
|
|
7956
|
-
|
|
8400
|
+
// Docker rejects an explicit hostname when the network namespace is another
|
|
8401
|
+
// container's — that container owns the UTS identity too.
|
|
8402
|
+
...sharesNetns ? {} : { Hostname: config.serviceName },
|
|
7957
8403
|
Labels: {
|
|
7958
8404
|
...this.labels({
|
|
7959
8405
|
deploymentId: config.deploymentId,
|
|
@@ -7962,23 +8408,31 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7962
8408
|
"openship.service": config.serviceName
|
|
7963
8409
|
},
|
|
7964
8410
|
...healthcheck && { Healthcheck: healthcheck },
|
|
7965
|
-
|
|
8411
|
+
...stopConfig,
|
|
8412
|
+
...ownsProjectEndpoint ? { ExposedPorts: exposedPorts } : {},
|
|
7966
8413
|
HostConfig: {
|
|
7967
8414
|
RestartPolicy: restartPolicy,
|
|
7968
8415
|
...dockerResourceLimits(config.resources),
|
|
7969
|
-
PortBindings: portBindings,
|
|
8416
|
+
...ownsProjectEndpoint ? { PortBindings: portBindings } : {},
|
|
7970
8417
|
Binds: binds,
|
|
7971
|
-
|
|
8418
|
+
// The project network is the default; a compose `network_mode` replaces it.
|
|
8419
|
+
NetworkMode: sharedNetwork ?? group.id,
|
|
8420
|
+
...sharedPid ? { PidMode: sharedPid } : {}
|
|
7972
8421
|
},
|
|
7973
|
-
|
|
7974
|
-
|
|
7975
|
-
|
|
7976
|
-
|
|
7977
|
-
|
|
7978
|
-
|
|
8422
|
+
// A declared network mode means there is no endpoint to name — omit the
|
|
8423
|
+
// block entirely rather than send an empty one (and never contradict
|
|
8424
|
+
// `NetworkMode: none` by attaching a network anyway).
|
|
8425
|
+
...ownsProjectEndpoint ? {
|
|
8426
|
+
NetworkingConfig: {
|
|
8427
|
+
EndpointsConfig: {
|
|
8428
|
+
[group.id]: {
|
|
8429
|
+
// Default service name plus any operator-chosen aliases; both
|
|
8430
|
+
// resolve to this container on the project network.
|
|
8431
|
+
Aliases: buildNetworkAliases(config.serviceName, config.extraAliases)
|
|
8432
|
+
}
|
|
7979
8433
|
}
|
|
7980
8434
|
}
|
|
7981
|
-
}
|
|
8435
|
+
} : {}
|
|
7982
8436
|
});
|
|
7983
8437
|
try {
|
|
7984
8438
|
await container.start();
|
|
@@ -8034,12 +8488,17 @@ export {
|
|
|
8034
8488
|
startExecStream,
|
|
8035
8489
|
startAttachStream,
|
|
8036
8490
|
daemonConnectionFrom,
|
|
8491
|
+
DEFAULT_DOCKER_SOCKET_PATH,
|
|
8492
|
+
resolveLocalDockerSocketPath,
|
|
8037
8493
|
ownsBuiltImage,
|
|
8038
8494
|
buildInContainerExecCmd,
|
|
8039
8495
|
resolveExecExitCode,
|
|
8040
8496
|
buildNetworkAliases,
|
|
8041
8497
|
parsePortBindings,
|
|
8042
8498
|
packBuildContext,
|
|
8499
|
+
toDockerHealthcheck,
|
|
8500
|
+
toStopConfig,
|
|
8501
|
+
gracefulStopForGrace,
|
|
8043
8502
|
splitDockerFrames,
|
|
8044
8503
|
parseContainerEventLine,
|
|
8045
8504
|
DockerRuntime
|