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
|
@@ -5,12 +5,12 @@ const require = __ospCreateRequire(import.meta.url);
|
|
|
5
5
|
import {
|
|
6
6
|
getTarCreateEnv,
|
|
7
7
|
prepareSourceTarArgs
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-Q7WGTQQ5.js";
|
|
9
9
|
import {
|
|
10
10
|
TRANSFER_EXCLUDES,
|
|
11
11
|
packageManagerEnsureCommand,
|
|
12
12
|
safeErrorMessage
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-UJ6JZM65.js";
|
|
14
14
|
|
|
15
15
|
// ../../packages/adapters/src/runtime/git-clone.ts
|
|
16
16
|
function sq(value) {
|
|
@@ -193,6 +193,19 @@ var BuildCancelledError = class extends Error {
|
|
|
193
193
|
this.name = "BuildCancelledError";
|
|
194
194
|
}
|
|
195
195
|
};
|
|
196
|
+
async function killProcessesUnderDir(executor, dir, opts) {
|
|
197
|
+
const quoted = sq(dir);
|
|
198
|
+
const roots = opts?.includeSuffixed ? [quoted, `${quoted}-*`] : [quoted];
|
|
199
|
+
const patterns = roots.flatMap((root) => [
|
|
200
|
+
root,
|
|
201
|
+
`${root}/*`,
|
|
202
|
+
`${root}" (deleted)"`,
|
|
203
|
+
`${root}/*" (deleted)"`
|
|
204
|
+
]);
|
|
205
|
+
const scan = (sig) => `for p in /proc/[0-9]*; do c=$(readlink "$p/cwd" 2>/dev/null); case "$c" in ${patterns.join("|")}) kill -${sig} "\${p##*/}" 2>/dev/null || true;; esac; done`;
|
|
206
|
+
await executor.exec(`${scan("TERM")}; sleep 2; ${scan("KILL")}`).catch(() => {
|
|
207
|
+
});
|
|
208
|
+
}
|
|
196
209
|
async function runBuildPipeline(env, config, logger) {
|
|
197
210
|
let startTime = Date.now();
|
|
198
211
|
let currentStep = "prepare";
|
|
@@ -413,6 +426,7 @@ export {
|
|
|
413
426
|
localGitSshWriter,
|
|
414
427
|
BuildLogger,
|
|
415
428
|
BuildCancelledError,
|
|
429
|
+
killProcessesUnderDir,
|
|
416
430
|
runBuildPipeline,
|
|
417
431
|
parseLogLevel,
|
|
418
432
|
detectBuildKillHint,
|
|
@@ -7,14 +7,14 @@ import {
|
|
|
7
7
|
localGitSshWriter,
|
|
8
8
|
materializeGitSsh,
|
|
9
9
|
sq
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-LH2PLRQO.js";
|
|
11
11
|
import {
|
|
12
12
|
getTarCreateEnv,
|
|
13
13
|
prepareSourceTarArgs
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-Q7WGTQQ5.js";
|
|
15
15
|
import {
|
|
16
16
|
packageManagerEnsureCommand
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-UJ6JZM65.js";
|
|
18
18
|
import {
|
|
19
19
|
__commonJS,
|
|
20
20
|
__toESM
|
|
@@ -480,16 +480,34 @@ var require_ignore = __commonJS({
|
|
|
480
480
|
|
|
481
481
|
// ../../packages/adapters/src/runtime/docker-paths.ts
|
|
482
482
|
import { access, readdir } from "fs/promises";
|
|
483
|
-
import { isAbsolute, join, relative, sep } from "path";
|
|
483
|
+
import { isAbsolute, join, relative, resolve, sep } from "path";
|
|
484
484
|
function toPosixPath(value) {
|
|
485
485
|
return value.split(sep).filter(Boolean).join("/");
|
|
486
486
|
}
|
|
487
|
+
function toSafeSegments(value, label) {
|
|
488
|
+
const segments = value.split(/[\\/]/).filter((segment) => segment && segment !== ".");
|
|
489
|
+
if (segments.includes("..")) {
|
|
490
|
+
throw new Error(
|
|
491
|
+
`Invalid ${label} ${JSON.stringify(value)}: path segments must not contain "..".`
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
return segments;
|
|
495
|
+
}
|
|
487
496
|
function normalizeDockerRelativePath(value) {
|
|
488
497
|
const normalized = value?.trim().replace(/^\.\//, "").replace(/^\/+|\/+$/g, "");
|
|
489
498
|
if (!normalized || normalized === ".") {
|
|
490
499
|
return "";
|
|
491
500
|
}
|
|
492
|
-
return normalized
|
|
501
|
+
return toSafeSegments(normalized, "path").join("/");
|
|
502
|
+
}
|
|
503
|
+
function resolveWithinDirectory(baseDir, relativePath) {
|
|
504
|
+
const base = resolve(baseDir);
|
|
505
|
+
const target = resolve(base, ...toSafeSegments(relativePath?.trim() ?? "", "path"));
|
|
506
|
+
const escape = relative(base, target);
|
|
507
|
+
if (escape === ".." || escape.startsWith(`..${sep}`) || isAbsolute(escape)) {
|
|
508
|
+
throw new Error(`Resolved path ${JSON.stringify(target)} escapes ${JSON.stringify(base)}.`);
|
|
509
|
+
}
|
|
510
|
+
return target;
|
|
493
511
|
}
|
|
494
512
|
function normalizeDockerRootDirectory(rootDirectory, localPath) {
|
|
495
513
|
let normalized = rootDirectory?.trim() ?? "";
|
|
@@ -509,7 +527,7 @@ function normalizeDockerRootDirectory(rootDirectory, localPath) {
|
|
|
509
527
|
if (!normalized || normalized === ".") {
|
|
510
528
|
return "";
|
|
511
529
|
}
|
|
512
|
-
return
|
|
530
|
+
return toSafeSegments(normalized, "rootDirectory").join("/");
|
|
513
531
|
}
|
|
514
532
|
function resolveExplicitDockerfileCandidate(rootDirectory, dockerfilePath) {
|
|
515
533
|
const normalizedRootDirectory = normalizeDockerRelativePath(rootDirectory);
|
|
@@ -904,7 +922,7 @@ import { join as join2, relative as relative2, sep as sep2 } from "path";
|
|
|
904
922
|
var GIT_CLONE_IDLE_TIMEOUT_MS = 5 * 6e4;
|
|
905
923
|
var GIT_CHECKOUT_IDLE_TIMEOUT_MS = 6e4;
|
|
906
924
|
function spawnGit(args, opts) {
|
|
907
|
-
return new Promise((
|
|
925
|
+
return new Promise((resolve2, reject) => {
|
|
908
926
|
const child = spawn("git", args, {
|
|
909
927
|
stdio: ["ignore", "pipe", "pipe"],
|
|
910
928
|
env: {
|
|
@@ -954,7 +972,7 @@ function spawnGit(args, opts) {
|
|
|
954
972
|
});
|
|
955
973
|
child.on("close", (code) => {
|
|
956
974
|
clearTimeout(idleTimer);
|
|
957
|
-
if (code === 0)
|
|
975
|
+
if (code === 0) resolve2();
|
|
958
976
|
else reject(new Error(`git exited with code ${code}: ${stderr.trim().slice(-500) || "no stderr"}`));
|
|
959
977
|
});
|
|
960
978
|
});
|
|
@@ -1014,7 +1032,7 @@ async function applyDockerignore(contextDir, config) {
|
|
|
1014
1032
|
async function materializeLocalSource(sourcePath, targetPath) {
|
|
1015
1033
|
const { args, cleanup } = await prepareSourceTarArgs(sourcePath);
|
|
1016
1034
|
try {
|
|
1017
|
-
await new Promise((
|
|
1035
|
+
await new Promise((resolve2, reject) => {
|
|
1018
1036
|
const create = spawn("tar", args, { env: getTarCreateEnv() });
|
|
1019
1037
|
const extract = spawn("tar", ["-xzf", "-", "-C", targetPath]);
|
|
1020
1038
|
let err = "";
|
|
@@ -1027,7 +1045,7 @@ async function materializeLocalSource(sourcePath, targetPath) {
|
|
|
1027
1045
|
create.on("close", (code) => createCode = code);
|
|
1028
1046
|
create.stdout.pipe(extract.stdin);
|
|
1029
1047
|
extract.on("close", (code) => {
|
|
1030
|
-
if (code === 0 && (createCode ?? 0) === 0) return
|
|
1048
|
+
if (code === 0 && (createCode ?? 0) === 0) return resolve2();
|
|
1031
1049
|
const parts = [
|
|
1032
1050
|
createCode ? `pack exited ${createCode}: ${createErr.trim().slice(-500)}` : "",
|
|
1033
1051
|
code ? `extract exited ${code}: ${err.trim().slice(-500)}` : ""
|
|
@@ -1173,6 +1191,7 @@ async function createDockerBuildContext(config, opts) {
|
|
|
1173
1191
|
|
|
1174
1192
|
export {
|
|
1175
1193
|
normalizeDockerRelativePath,
|
|
1194
|
+
resolveWithinDirectory,
|
|
1176
1195
|
resolveDockerfileCandidates,
|
|
1177
1196
|
staticBuilderOutputPath,
|
|
1178
1197
|
generateDockerfile,
|
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
import { createRequire as __ospCreateRequire } from "node:module";
|
|
4
4
|
const require = __ospCreateRequire(import.meta.url);
|
|
5
5
|
import {
|
|
6
|
-
connectSshClient,
|
|
7
6
|
getTarCreateEnv,
|
|
7
|
+
prepareSourceTarArgs
|
|
8
|
+
} from "./chunk-Q7WGTQQ5.js";
|
|
9
|
+
import {
|
|
10
|
+
connectSshClient,
|
|
11
|
+
openDockerDialStdioChannel,
|
|
8
12
|
openSftp,
|
|
9
13
|
openSshUnixSocket,
|
|
10
|
-
prepareSourceTarArgs,
|
|
11
14
|
reconcileKnownHosts
|
|
12
|
-
} from "./chunk-
|
|
13
|
-
import {
|
|
14
|
-
SshDisconnectedError
|
|
15
|
-
} from "./chunk-ZCFVHA25.js";
|
|
15
|
+
} from "./chunk-PCPVTH6M.js";
|
|
16
16
|
import {
|
|
17
17
|
LocalExecutor
|
|
18
18
|
} from "./chunk-6FH4AXVY.js";
|
|
@@ -24,13 +24,80 @@ import {
|
|
|
24
24
|
sq
|
|
25
25
|
} from "./chunk-ZFJXPCQP.js";
|
|
26
26
|
import {
|
|
27
|
+
HOST_CHANNEL_AUTH_REJECTED,
|
|
28
|
+
HOST_CHANNEL_NOT_PROVISIONED,
|
|
29
|
+
HOST_CHANNEL_UNPROVISIONED,
|
|
30
|
+
HostChannelUnavailableError,
|
|
31
|
+
SshDisconnectedError,
|
|
27
32
|
TRANSFER_EXCLUDES,
|
|
33
|
+
commandForError,
|
|
34
|
+
explainHostChannelCause,
|
|
28
35
|
formatBytes,
|
|
36
|
+
hostChannelAccount,
|
|
37
|
+
hostFirewallRule,
|
|
38
|
+
isSshAuthError,
|
|
29
39
|
safeErrorMessage
|
|
30
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-UJ6JZM65.js";
|
|
31
41
|
|
|
32
42
|
// ../../packages/adapters/src/system/executor.ts
|
|
33
43
|
import { existsSync, readFileSync } from "fs";
|
|
44
|
+
import { networkInterfaces } from "os";
|
|
45
|
+
|
|
46
|
+
// ../../packages/adapters/src/system/reachability.ts
|
|
47
|
+
import { connect } from "net";
|
|
48
|
+
import { request } from "http";
|
|
49
|
+
function classifyDialError(code) {
|
|
50
|
+
switch (code) {
|
|
51
|
+
case "ECONNREFUSED":
|
|
52
|
+
case "ECONNRESET":
|
|
53
|
+
return "refused";
|
|
54
|
+
case "ENOTFOUND":
|
|
55
|
+
case "EAI_AGAIN":
|
|
56
|
+
return "unresolved";
|
|
57
|
+
case "EHOSTUNREACH":
|
|
58
|
+
case "ENETUNREACH":
|
|
59
|
+
case "ENETDOWN":
|
|
60
|
+
return "no_route";
|
|
61
|
+
// The kernel's own connect timeout, i.e. our `timeout` reported for us.
|
|
62
|
+
case "ETIMEDOUT":
|
|
63
|
+
return "timeout";
|
|
64
|
+
default:
|
|
65
|
+
return "error";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function probeTcpDetailed(host, port, timeoutMs = 2500) {
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
let settled = false;
|
|
71
|
+
const done = (result, socket2) => {
|
|
72
|
+
if (settled) return;
|
|
73
|
+
settled = true;
|
|
74
|
+
try {
|
|
75
|
+
socket2?.destroy();
|
|
76
|
+
} catch {
|
|
77
|
+
}
|
|
78
|
+
resolve(result);
|
|
79
|
+
};
|
|
80
|
+
const socket = connect({ host, port });
|
|
81
|
+
socket.setTimeout(timeoutMs);
|
|
82
|
+
socket.once("connect", () => done({ ok: true }, socket));
|
|
83
|
+
socket.once(
|
|
84
|
+
"timeout",
|
|
85
|
+
() => done({ ok: false, reason: "timeout", message: `no response within ${timeoutMs}ms` }, socket)
|
|
86
|
+
);
|
|
87
|
+
socket.once("error", (err) => {
|
|
88
|
+
const errno = err;
|
|
89
|
+
done(
|
|
90
|
+
{
|
|
91
|
+
ok: false,
|
|
92
|
+
reason: classifyDialError(errno.code),
|
|
93
|
+
...errno.code ? { code: errno.code } : {},
|
|
94
|
+
...errno.message ? { message: errno.message } : {}
|
|
95
|
+
},
|
|
96
|
+
socket
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
34
101
|
|
|
35
102
|
// ../../packages/adapters/src/system/ssh-executor.ts
|
|
36
103
|
import { createReadStream } from "fs";
|
|
@@ -240,7 +307,7 @@ var SshExecutor = class _SshExecutor {
|
|
|
240
307
|
async connect() {
|
|
241
308
|
if (this.client) return this.client;
|
|
242
309
|
if (this.connecting) return this.connecting;
|
|
243
|
-
|
|
310
|
+
const attempt = (async () => {
|
|
244
311
|
const client = await connectSshClient(this.config);
|
|
245
312
|
const onTransportDown = (cause) => {
|
|
246
313
|
if (this.client !== client) return;
|
|
@@ -252,10 +319,14 @@ var SshExecutor = class _SshExecutor {
|
|
|
252
319
|
client.on("end", () => onTransportDown());
|
|
253
320
|
client.on("error", (err) => onTransportDown(err));
|
|
254
321
|
this.client = client;
|
|
255
|
-
this.connecting = null;
|
|
256
322
|
return client;
|
|
257
323
|
})();
|
|
258
|
-
|
|
324
|
+
this.connecting = attempt;
|
|
325
|
+
const clearMemo = () => {
|
|
326
|
+
if (this.connecting === attempt) this.connecting = null;
|
|
327
|
+
};
|
|
328
|
+
attempt.then(clearMemo, clearMemo);
|
|
329
|
+
return attempt;
|
|
259
330
|
}
|
|
260
331
|
/**
|
|
261
332
|
* Subscribe to transport-level disconnects. Returns an unsubscribe fn.
|
|
@@ -402,7 +473,9 @@ var SshExecutor = class _SshExecutor {
|
|
|
402
473
|
};
|
|
403
474
|
this.inflight.add(abort);
|
|
404
475
|
timer = setTimeout(
|
|
405
|
-
() => finish(
|
|
476
|
+
() => finish(
|
|
477
|
+
() => reject(new Error(`Command timed out after ${timeout}ms: ${commandForError(command)}`))
|
|
478
|
+
),
|
|
406
479
|
timeout
|
|
407
480
|
);
|
|
408
481
|
client.exec(_SshExecutor.ENV_PREFIX + command, (err, stream) => {
|
|
@@ -706,24 +779,23 @@ var SshExecutor = class _SshExecutor {
|
|
|
706
779
|
* Carry the Docker Engine API over a `docker system dial-stdio` exec channel
|
|
707
780
|
* on the pooled connection — the streamlocal-free transport used when the
|
|
708
781
|
* SSH server (or the Bun-compiled desktop runtime) can't do socket
|
|
709
|
-
* forwarding.
|
|
710
|
-
*
|
|
782
|
+
* forwarding.
|
|
783
|
+
*
|
|
784
|
+
* Deliberately WITHOUT `ENV_PREFIX`: that prefix only exports apt's
|
|
785
|
+
* non-interactive variables (it sets no PATH, despite what the comment here
|
|
786
|
+
* used to claim), and the bridge's own ephemeral dial-stdio client opens the
|
|
787
|
+
* bare command. Two dial-stdio channels that differ by a shell prefix are two
|
|
788
|
+
* transports, and the bridge picks one by probing the other.
|
|
789
|
+
*
|
|
790
|
+
* Failure diagnostics (daemon down, socket permission, `docker: command not
|
|
791
|
+
* found`, an sshd ForceCommand banner) ride ON the returned channel — see
|
|
792
|
+
* `attachDialStdioDiagnostics`. They used to go to `console.warn` while the
|
|
793
|
+
* caller got a bare socket reset, which is how a host with no running dockerd
|
|
794
|
+
* reported itself as `socket hang up`.
|
|
711
795
|
*/
|
|
712
796
|
async openDockerDialStdio() {
|
|
713
797
|
const client = await this.connect();
|
|
714
|
-
return
|
|
715
|
-
client.exec(_SshExecutor.ENV_PREFIX + "docker system dial-stdio", (err, stream) => {
|
|
716
|
-
if (err) return reject(err);
|
|
717
|
-
stream.stderr?.on("data", (d) => {
|
|
718
|
-
const text = d.toString().trim();
|
|
719
|
-
if (text) console.warn(`[docker-ssh] dial-stdio stderr: ${text}`);
|
|
720
|
-
});
|
|
721
|
-
stream.on("exit", (code) => {
|
|
722
|
-
if (code) console.warn(`[docker-ssh] dial-stdio exited early (code=${code})`);
|
|
723
|
-
});
|
|
724
|
-
resolve(stream);
|
|
725
|
-
});
|
|
726
|
-
});
|
|
798
|
+
return openDockerDialStdioChannel(client);
|
|
727
799
|
}
|
|
728
800
|
async forwardPort(remoteHost, remotePort) {
|
|
729
801
|
const client = await this.connect();
|
|
@@ -1200,7 +1272,7 @@ var SystemSshExecutor = class _SystemSshExecutor {
|
|
|
1200
1272
|
const timeout = opts?.timeout ?? 3e4;
|
|
1201
1273
|
const res = await this.runSsh(_SystemSshExecutor.ENV_PREFIX + command, { timeout });
|
|
1202
1274
|
if (res.timedOut) {
|
|
1203
|
-
throw new Error(`Command timed out after ${timeout}ms: ${command}`);
|
|
1275
|
+
throw new Error(`Command timed out after ${timeout}ms: ${commandForError(command)}`);
|
|
1204
1276
|
}
|
|
1205
1277
|
if (res.code !== 0) {
|
|
1206
1278
|
if (res.code === 255) throw describeSshFailure(res.stderr, `Exit code ${res.code}`);
|
|
@@ -1208,14 +1280,22 @@ var SystemSshExecutor = class _SystemSshExecutor {
|
|
|
1208
1280
|
}
|
|
1209
1281
|
return res.stdout.trim();
|
|
1210
1282
|
}
|
|
1211
|
-
async streamExec(command, onLog) {
|
|
1283
|
+
async streamExec(command, onLog, opts) {
|
|
1212
1284
|
await this.ensureMaster();
|
|
1285
|
+
const signal = opts?.signal;
|
|
1286
|
+
if (signal?.aborted) return { code: 0, output: "" };
|
|
1213
1287
|
return new Promise((resolve) => {
|
|
1214
1288
|
const child = spawn2(
|
|
1215
1289
|
"ssh",
|
|
1216
1290
|
[...this.baseArgs(), sshTarget(this.config), _SystemSshExecutor.ENV_PREFIX + command],
|
|
1217
1291
|
{ env: sshChildEnv(this.config), stdio: ["ignore", "pipe", "pipe"] }
|
|
1218
1292
|
);
|
|
1293
|
+
let aborted = false;
|
|
1294
|
+
const onAbort = () => {
|
|
1295
|
+
aborted = true;
|
|
1296
|
+
child.kill("SIGKILL");
|
|
1297
|
+
};
|
|
1298
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1219
1299
|
const chunks = [];
|
|
1220
1300
|
const onChunk = (chunk, level) => {
|
|
1221
1301
|
const text = chunk.toString();
|
|
@@ -1226,11 +1306,13 @@ var SystemSshExecutor = class _SystemSshExecutor {
|
|
|
1226
1306
|
child.stdout.on("data", (chunk) => onChunk(chunk, "info"));
|
|
1227
1307
|
child.stderr.on("data", (chunk) => onChunk(chunk, "warn"));
|
|
1228
1308
|
child.on("error", (err) => {
|
|
1309
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1229
1310
|
onLog(logEntry(`Process error: ${err.message}`, "error"));
|
|
1230
1311
|
resolve({ code: 1, output: err.message });
|
|
1231
1312
|
});
|
|
1232
1313
|
child.on("close", (code) => {
|
|
1233
|
-
|
|
1314
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1315
|
+
const c = aborted ? 0 : code ?? 1;
|
|
1234
1316
|
void this.maybeSignalDisconnect(c).catch(() => {
|
|
1235
1317
|
});
|
|
1236
1318
|
resolve({ code: c, output: chunks.join("") });
|
|
@@ -1524,8 +1606,12 @@ function createExecutor(ssh) {
|
|
|
1524
1606
|
}
|
|
1525
1607
|
return localExecutor;
|
|
1526
1608
|
}
|
|
1609
|
+
var hostControlOverride = null;
|
|
1610
|
+
function setHostControlOverride(disabled) {
|
|
1611
|
+
hostControlOverride = disabled;
|
|
1612
|
+
}
|
|
1527
1613
|
function hostControlDisabled() {
|
|
1528
|
-
return process.env.OPENSHIP_HOST_CONTROL?.trim().toLowerCase() === "false";
|
|
1614
|
+
return hostControlOverride ?? process.env.OPENSHIP_HOST_CONTROL?.trim().toLowerCase() === "false";
|
|
1529
1615
|
}
|
|
1530
1616
|
function runningInContainer() {
|
|
1531
1617
|
if (process.env.OPENSHIP_IN_CONTAINER?.trim().toLowerCase() === "true") return true;
|
|
@@ -1539,39 +1625,253 @@ function runningInContainer() {
|
|
|
1539
1625
|
return false;
|
|
1540
1626
|
}
|
|
1541
1627
|
}
|
|
1628
|
+
function hostChannelPort() {
|
|
1629
|
+
const raw = Number(process.env.OPENSHIP_HOST_SSH_PORT || "22");
|
|
1630
|
+
return Number.isInteger(raw) && raw > 0 && raw < 65536 ? raw : 22;
|
|
1631
|
+
}
|
|
1632
|
+
function hostChannelUser() {
|
|
1633
|
+
return hostChannelAccount(process.env);
|
|
1634
|
+
}
|
|
1635
|
+
function readHostChannelKey(keyPath) {
|
|
1636
|
+
let contents;
|
|
1637
|
+
try {
|
|
1638
|
+
contents = readFileSync(keyPath, "utf8");
|
|
1639
|
+
} catch (err) {
|
|
1640
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1641
|
+
return { reason: `Cannot read the host SSH key at ${keyPath} (${detail}).` };
|
|
1642
|
+
}
|
|
1643
|
+
if (!contents.trim()) return { reason: `The host SSH key at ${keyPath} is empty.` };
|
|
1644
|
+
return { key: contents };
|
|
1645
|
+
}
|
|
1646
|
+
function containerBridgeCidr() {
|
|
1647
|
+
for (const addrs of Object.values(networkInterfaces())) {
|
|
1648
|
+
for (const addr of addrs ?? []) {
|
|
1649
|
+
if (addr.family !== "IPv4" || addr.internal) continue;
|
|
1650
|
+
const prefix = Number(addr.cidr?.split("/")[1]);
|
|
1651
|
+
if (!Number.isInteger(prefix) || prefix < 8 || prefix > 32) continue;
|
|
1652
|
+
const octets = addr.address.split(".").map(Number);
|
|
1653
|
+
if (octets.length !== 4 || octets.some((o) => !Number.isInteger(o) || o < 0 || o > 255)) continue;
|
|
1654
|
+
const asInt = (octets[0] << 24 | octets[1] << 16 | octets[2] << 8 | octets[3]) >>> 0;
|
|
1655
|
+
const mask = 4294967295 << 32 - prefix >>> 0;
|
|
1656
|
+
const net = (asInt & mask) >>> 0;
|
|
1657
|
+
return `${net >>> 24 & 255}.${net >>> 16 & 255}.${net >>> 8 & 255}.${net & 255}/${prefix}`;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
return null;
|
|
1661
|
+
}
|
|
1662
|
+
function probeDetail(probe) {
|
|
1663
|
+
const { code, message } = probe;
|
|
1664
|
+
if (code && message && !message.includes(code)) return `${code}: ${message}`;
|
|
1665
|
+
return code ?? message;
|
|
1666
|
+
}
|
|
1667
|
+
function explainDialFailure(probe, host, port, target) {
|
|
1668
|
+
const { headline, body, firewallShaped } = explainHostChannelCause(probe.reason, {
|
|
1669
|
+
target,
|
|
1670
|
+
host,
|
|
1671
|
+
detail: probeDetail(probe)
|
|
1672
|
+
});
|
|
1673
|
+
const hint = body ? `${headline}. ${body}` : `${headline}.`;
|
|
1674
|
+
if (!firewallShaped) return { hint };
|
|
1675
|
+
const cidr = containerBridgeCidr();
|
|
1676
|
+
return { hint, rule: hostFirewallRule("unknown", cidr ? [cidr] : [], port) };
|
|
1677
|
+
}
|
|
1678
|
+
var AUTH_MEMO_OK_MS = 3e4;
|
|
1679
|
+
var AUTH_MEMO_FAIL_MS = 5e3;
|
|
1680
|
+
var authMemo = null;
|
|
1681
|
+
function invalidateHostChannelAuth() {
|
|
1682
|
+
authMemo = null;
|
|
1683
|
+
}
|
|
1684
|
+
function probeForwarding(client, timeoutMs) {
|
|
1685
|
+
return new Promise((resolve) => {
|
|
1686
|
+
let settled = false;
|
|
1687
|
+
const finish = (verdict) => {
|
|
1688
|
+
if (settled) return;
|
|
1689
|
+
settled = true;
|
|
1690
|
+
clearTimeout(timer);
|
|
1691
|
+
resolve(verdict);
|
|
1692
|
+
};
|
|
1693
|
+
const timer = setTimeout(() => finish("unknown"), timeoutMs);
|
|
1694
|
+
timer.unref?.();
|
|
1695
|
+
try {
|
|
1696
|
+
client.forwardOut("127.0.0.1", 0, "127.0.0.1", 1, (err, stream) => {
|
|
1697
|
+
if (stream) {
|
|
1698
|
+
stream.destroy();
|
|
1699
|
+
finish("ok");
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
const reason = err?.reason;
|
|
1703
|
+
const message = err?.message;
|
|
1704
|
+
if (reason === 1 || reason === 3) return finish("blocked");
|
|
1705
|
+
if (typeof message === "string" && /administratively prohibited/i.test(message)) {
|
|
1706
|
+
return finish("blocked");
|
|
1707
|
+
}
|
|
1708
|
+
finish("ok");
|
|
1709
|
+
});
|
|
1710
|
+
} catch {
|
|
1711
|
+
finish("unknown");
|
|
1712
|
+
}
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
async function verifyHostChannelAuth(config) {
|
|
1716
|
+
const memoKey = `${config.username}@${config.host}:${config.port}#${config.privateKey.length}`;
|
|
1717
|
+
if (authMemo?.key === memoKey && authMemo.expires > Date.now()) {
|
|
1718
|
+
return authMemo.hint ? { hint: authMemo.hint, forwarding: authMemo.forwarding } : { forwarding: authMemo.forwarding };
|
|
1719
|
+
}
|
|
1720
|
+
let hint = null;
|
|
1721
|
+
let forwarding = "unknown";
|
|
1722
|
+
try {
|
|
1723
|
+
const { connectSshClient: connectSshClient2 } = await import("./ssh-client-JBMA7SN4.js");
|
|
1724
|
+
const client = await connectSshClient2({
|
|
1725
|
+
host: config.host,
|
|
1726
|
+
port: config.port,
|
|
1727
|
+
username: config.username,
|
|
1728
|
+
privateKey: config.privateKey,
|
|
1729
|
+
readyTimeoutMs: config.timeoutMs,
|
|
1730
|
+
// Load-bearing: it selects describeSshAuthFailure's host-channel wording, so the
|
|
1731
|
+
// hint an operator reads here is the same sentence the deploy log gives them.
|
|
1732
|
+
hostChannel: true
|
|
1733
|
+
});
|
|
1734
|
+
forwarding = await probeForwarding(
|
|
1735
|
+
client,
|
|
1736
|
+
config.timeoutMs
|
|
1737
|
+
);
|
|
1738
|
+
client.end();
|
|
1739
|
+
} catch (err) {
|
|
1740
|
+
if (isSshAuthError(err)) {
|
|
1741
|
+
hint = err instanceof Error ? err.message : HOST_CHANNEL_AUTH_REJECTED;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
authMemo = {
|
|
1745
|
+
key: memoKey,
|
|
1746
|
+
hint,
|
|
1747
|
+
forwarding,
|
|
1748
|
+
expires: Date.now() + (hint ? AUTH_MEMO_FAIL_MS : AUTH_MEMO_OK_MS)
|
|
1749
|
+
};
|
|
1750
|
+
return hint ? { hint, forwarding } : { forwarding };
|
|
1751
|
+
}
|
|
1752
|
+
async function hostChannelHealth(timeoutMs = 2500) {
|
|
1753
|
+
if (hostControlDisabled()) {
|
|
1754
|
+
return {
|
|
1755
|
+
ok: false,
|
|
1756
|
+
code: "disabled",
|
|
1757
|
+
hint: "Host control is off (OPENSHIP_HOST_CONTROL=false). Re-run `openship up` without --no-host-control."
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
const host = process.env.OPENSHIP_HOST_SSH_HOST?.trim();
|
|
1761
|
+
if (!host) {
|
|
1762
|
+
if (runningInContainer()) {
|
|
1763
|
+
return {
|
|
1764
|
+
ok: false,
|
|
1765
|
+
code: "not_configured",
|
|
1766
|
+
hint: `${HOST_CHANNEL_UNPROVISIONED} ${HOST_CHANNEL_NOT_PROVISIONED}`
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
return { ok: true, code: "not_applicable" };
|
|
1770
|
+
}
|
|
1771
|
+
const port = hostChannelPort();
|
|
1772
|
+
const target = `${hostChannelUser()}@${host}:${port}`;
|
|
1773
|
+
const keyPath = process.env.OPENSHIP_HOST_SSH_KEY?.trim();
|
|
1774
|
+
let privateKey;
|
|
1775
|
+
if (keyPath) {
|
|
1776
|
+
const read = readHostChannelKey(keyPath);
|
|
1777
|
+
if (read.reason) {
|
|
1778
|
+
return {
|
|
1779
|
+
ok: false,
|
|
1780
|
+
code: "key_unreadable",
|
|
1781
|
+
host,
|
|
1782
|
+
port,
|
|
1783
|
+
target,
|
|
1784
|
+
hint: `${read.reason} ${HOST_CHANNEL_NOT_PROVISIONED}`
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
privateKey = read.key;
|
|
1788
|
+
}
|
|
1789
|
+
const probe = await probeTcpDetailed(host, port, timeoutMs);
|
|
1790
|
+
if (!probe.ok) {
|
|
1791
|
+
return {
|
|
1792
|
+
ok: false,
|
|
1793
|
+
code: "unreachable",
|
|
1794
|
+
host,
|
|
1795
|
+
port,
|
|
1796
|
+
target,
|
|
1797
|
+
cause: probe.reason,
|
|
1798
|
+
...explainDialFailure(probe, host, port, target)
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
let forwarding = "unknown";
|
|
1802
|
+
if (privateKey) {
|
|
1803
|
+
const verdict = await verifyHostChannelAuth({
|
|
1804
|
+
host,
|
|
1805
|
+
port,
|
|
1806
|
+
username: hostChannelUser(),
|
|
1807
|
+
privateKey,
|
|
1808
|
+
timeoutMs
|
|
1809
|
+
});
|
|
1810
|
+
if (verdict.hint) {
|
|
1811
|
+
return { ok: false, code: "auth_rejected", host, port, target, hint: verdict.hint };
|
|
1812
|
+
}
|
|
1813
|
+
forwarding = verdict.forwarding;
|
|
1814
|
+
}
|
|
1815
|
+
return { ok: true, code: "ok", host, port, target, forwarding };
|
|
1816
|
+
}
|
|
1817
|
+
function unavailableExecutor(reason, code = "not_configured") {
|
|
1818
|
+
const refuse = async () => {
|
|
1819
|
+
throw new HostChannelUnavailableError(code, reason);
|
|
1820
|
+
};
|
|
1821
|
+
return {
|
|
1822
|
+
exec: refuse,
|
|
1823
|
+
streamExec: refuse,
|
|
1824
|
+
writeFile: refuse,
|
|
1825
|
+
readFile: refuse,
|
|
1826
|
+
exists: refuse,
|
|
1827
|
+
mkdir: refuse,
|
|
1828
|
+
rm: refuse,
|
|
1829
|
+
transferIn: refuse,
|
|
1830
|
+
// Nothing was ever opened, so releasing must not be the one call that throws.
|
|
1831
|
+
dispose: async () => {
|
|
1832
|
+
}
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1542
1835
|
function createHostExecutor() {
|
|
1543
1836
|
if (hostControlDisabled()) {
|
|
1544
|
-
throw new
|
|
1837
|
+
throw new HostChannelUnavailableError(
|
|
1838
|
+
"disabled",
|
|
1545
1839
|
"Host control is disabled on this instance (OPENSHIP_HOST_CONTROL=false). Re-run `openship up` without --no-host-control to allow host operations."
|
|
1546
1840
|
);
|
|
1547
1841
|
}
|
|
1548
1842
|
const host = process.env.OPENSHIP_HOST_SSH_HOST?.trim();
|
|
1549
1843
|
if (!host) {
|
|
1550
1844
|
if (runningInContainer()) {
|
|
1551
|
-
throw new
|
|
1552
|
-
"
|
|
1845
|
+
throw new HostChannelUnavailableError(
|
|
1846
|
+
"not_configured",
|
|
1847
|
+
`This operation targets the HOST machine, but no host channel is configured (OPENSHIP_HOST_SSH_HOST is unset) and Openship is running in a container \u2014 so it would have run inside the container instead, against the wrong filesystem. ${HOST_CHANNEL_NOT_PROVISIONED}`
|
|
1553
1848
|
);
|
|
1554
1849
|
}
|
|
1555
1850
|
return localExecutor;
|
|
1556
1851
|
}
|
|
1557
1852
|
const keyPath = process.env.OPENSHIP_HOST_SSH_KEY?.trim();
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1853
|
+
if (!keyPath) {
|
|
1854
|
+
throw new HostChannelUnavailableError(
|
|
1855
|
+
"not_configured",
|
|
1856
|
+
`A host channel is configured (OPENSHIP_HOST_SSH_HOST) but OPENSHIP_HOST_SSH_KEY is unset, so there is no credential to connect to the host with. ${HOST_CHANNEL_NOT_PROVISIONED}`
|
|
1857
|
+
);
|
|
1858
|
+
}
|
|
1859
|
+
const read = readHostChannelKey(keyPath);
|
|
1860
|
+
if (read.reason) {
|
|
1861
|
+
throw new HostChannelUnavailableError(
|
|
1862
|
+
"not_configured",
|
|
1863
|
+
`${read.reason} ${HOST_CHANNEL_NOT_PROVISIONED}`
|
|
1864
|
+
);
|
|
1569
1865
|
}
|
|
1570
1866
|
return new SshExecutor({
|
|
1571
1867
|
host,
|
|
1572
|
-
port,
|
|
1573
|
-
username:
|
|
1574
|
-
privateKey
|
|
1868
|
+
port: hostChannelPort(),
|
|
1869
|
+
username: hostChannelUser(),
|
|
1870
|
+
privateKey: read.key,
|
|
1871
|
+
// One hop over the docker bridge: it answers at once or it is being filtered.
|
|
1872
|
+
// 20s of silence per host operation reads as a hang, not as a failure.
|
|
1873
|
+
readyTimeoutMs: 8e3,
|
|
1874
|
+
hostChannel: true
|
|
1575
1875
|
});
|
|
1576
1876
|
}
|
|
1577
1877
|
|
|
@@ -1579,6 +1879,11 @@ export {
|
|
|
1579
1879
|
SshExecutor,
|
|
1580
1880
|
SystemSshExecutor,
|
|
1581
1881
|
createExecutor,
|
|
1882
|
+
setHostControlOverride,
|
|
1582
1883
|
hostControlDisabled,
|
|
1884
|
+
containerBridgeCidr,
|
|
1885
|
+
invalidateHostChannelAuth,
|
|
1886
|
+
hostChannelHealth,
|
|
1887
|
+
unavailableExecutor,
|
|
1583
1888
|
createHostExecutor
|
|
1584
1889
|
};
|