openship 0.6.6 → 0.6.8
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/dist/bare-FA66N7GC.js +29 -0
- package/dist/{chunk-77QDF6UD.js → chunk-3MP6ACKM.js} +587 -156
- package/dist/{chunk-YIMOCZBZ.js → chunk-5DJOB7IX.js} +3 -3
- package/dist/{chunk-WVAML4H6.js → chunk-5I3COAFH.js} +274 -65
- package/dist/{chunk-5DMWSTOM.js → chunk-5UXHT6ED.js} +940 -224
- package/dist/{chunk-LUKYPVJ4.js → chunk-6GU6JSK6.js} +329 -466
- package/dist/{chunk-RSR7PFIQ.js → chunk-7PEXM2UI.js} +213 -45
- package/dist/{chunk-VTGPEC6B.js → chunk-7YBWXEM6.js} +6 -4
- package/dist/chunk-AHJYP7DG.js +938 -0
- package/dist/chunk-BO6CY4V2.js +265 -0
- package/dist/{chunk-FE7ZZQSE.js → chunk-D3OEMXYP.js} +3 -3
- package/dist/chunk-D6HU4VUZ.js +249 -0
- package/dist/{chunk-LFE5KQ7Y.js → chunk-ENKJ7XIF.js} +152 -57
- package/dist/{chunk-YMMQRE5J.js → chunk-GNOMEMNI.js} +1 -1
- package/dist/{chunk-QVW76WDL.js → chunk-IAMOUDHY.js} +190 -64
- package/dist/{chunk-OBPKYXQL.js → chunk-MUWVC5R4.js} +219 -58
- package/dist/chunk-N5YZMO33.js +111 -0
- package/dist/{chunk-A4GXYE4L.js → chunk-NIYD7MD2.js} +206 -52
- package/dist/{chunk-UVNMQM7W.js → chunk-NPSRGIHV.js} +8 -6
- package/dist/{chunk-2GWYYHOR.js → chunk-NRU2EBYZ.js} +2 -2
- package/dist/{chunk-CBLD2HS5.js → chunk-NUT6HKK6.js} +8 -5
- package/dist/{chunk-ZFJXPCQP.js → chunk-P7RCYELG.js} +4 -3
- package/dist/chunk-R4VDPFLS.js +106 -0
- package/dist/{chunk-HKRQXPTI.js → chunk-R5DC5PEF.js} +1 -1
- package/dist/{chunk-NK5N5LXN.js → chunk-RCMERVBL.js} +24 -9
- package/dist/{chunk-RXQBD4ZY.js → chunk-RHLNVZMS.js} +2 -2
- package/dist/chunk-SK4FFCFR.js +70 -0
- package/dist/{chunk-4I2BM2DT.js → chunk-T46BQOLY.js} +3 -3
- package/dist/{chunk-6FH4AXVY.js → chunk-T6DVUX4F.js} +1 -1
- package/dist/{chunk-PCPVTH6M.js → chunk-TAZZZKYO.js} +6 -4
- package/dist/{chunk-MLYSEZQB.js → chunk-UAWY6X7V.js} +43 -72
- package/dist/{chunk-Q7WGTQQ5.js → chunk-VAI5VOOI.js} +1 -1
- package/dist/{chunk-UJ6JZM65.js → chunk-WEZFTL4V.js} +1325 -513
- package/dist/{chunk-2YM3NI2E.js → chunk-X53JXRS4.js} +38 -44
- package/dist/{cloud-KRTZ2IL5.js → cloud-2CRDKOWQ.js} +1 -1
- package/dist/cloud-REV3DPJH.js +42 -0
- package/dist/{detect-OIE5V3VZ.js → detect-BBFT5GEU.js} +9 -6
- package/dist/{docker-VTR2GK5Y.js → docker-EG3QSE6I.js} +11 -8
- package/dist/{docker-edge-executor-22IBXNYJ.js → docker-edge-executor-E5TOTLMR.js} +8 -6
- package/dist/edge-import-YJVJUGYC.js +56 -0
- package/dist/{edge-preflight-GBNCPNVU.js → edge-preflight-QUW4BM7N.js} +15 -12
- package/dist/{ensure-container-edge-KPI2CVXJ.js → ensure-container-edge-FOEFQ3PI.js} +15 -10
- package/dist/{executor-7LXXZPBR.js → executor-NTWDJ3DB.js} +8 -6
- package/dist/host-channel-preflight-OMCZMMKR.js +54 -0
- package/dist/index.js +198 -76
- package/dist/{local-executor-WOFZD72X.js → local-executor-OQ2GRN7K.js} +3 -2
- package/dist/{nginx-KUFMLMAC.js → nginx-4U4C2XUS.js} +11 -8
- package/dist/{noop-NJ72F46T.js → noop-CLLJHCPL.js} +1 -1
- package/dist/{openresty-lua-ATTH35BE.js → openresty-lua-LLWT5XQW.js} +8 -5
- package/dist/{privilege-I4W3ZNZK.js → privilege-MNDVMDST.js} +6 -5
- package/dist/server/index.js +40049 -30378
- package/dist/server/migrations/0109_service_build_args.sql +9 -0
- package/dist/server/migrations/0110_service_deployment_host_ports.sql +18 -0
- package/dist/server/migrations/0111_host_port_claim.sql +381 -0
- package/dist/server/migrations/0112_backup_run_execution_lease.sql +12 -0
- package/dist/server/migrations/0113_docker_migration_execution_lease.sql +11 -0
- package/dist/server/migrations/0114_build_session_execution_lease_index.sql +6 -0
- package/dist/server/migrations/0115_orphan_target_key.sql +3 -0
- package/dist/server/migrations/0116_domain_ssl_challenge.sql +2 -0
- package/dist/server/migrations/meta/_journal.json +56 -0
- package/dist/setup-IFIMPQZJ.js +25 -0
- package/dist/{ssh-client-JBMA7SN4.js → ssh-client-2XWZCMGI.js} +3 -2
- package/dist/up-HQS7IR7H.js +61 -0
- package/package.json +1 -1
- package/dist/bare-OXJVW3AK.js +0 -25
- package/dist/chunk-LH2PLRQO.js +0 -434
- package/dist/chunk-SM26HDS2.js +0 -185
- package/dist/cloud-75VIT4KV.js +0 -37
- package/dist/edge-import-WJUB4M7F.js +0 -52
- package/dist/host-channel-preflight-KE6OONYK.js +0 -50
- package/dist/setup-XGLGUUFW.js +0 -22
- package/dist/up-K4BV7466.js +0 -57
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
":" //# ; exec "$(command -v node || command -v bun)" "$0" "$@"
|
|
3
|
+
import { createRequire as __ospCreateRequire } from "node:module";
|
|
4
|
+
const require = __ospCreateRequire(import.meta.url);
|
|
5
|
+
import {
|
|
6
|
+
containerIdFromCgroup,
|
|
7
|
+
isContainerPortForwarder,
|
|
8
|
+
managedDeploymentIdFromUnit,
|
|
9
|
+
tryExec
|
|
10
|
+
} from "./chunk-D6HU4VUZ.js";
|
|
11
|
+
import {
|
|
12
|
+
shellQuote
|
|
13
|
+
} from "./chunk-WEZFTL4V.js";
|
|
14
|
+
|
|
15
|
+
// ../../packages/adapters/src/system/port-listen.ts
|
|
16
|
+
var PROC_NET_TCP_FILES = ["/proc/net/tcp", "/proc/net/tcp6"];
|
|
17
|
+
var TCP_LISTEN = "0A";
|
|
18
|
+
function parseListeningPorts(procText) {
|
|
19
|
+
const ports = /* @__PURE__ */ new Set();
|
|
20
|
+
for (const rawLine of procText.split("\n")) {
|
|
21
|
+
const line = rawLine.trim();
|
|
22
|
+
if (!line) continue;
|
|
23
|
+
const fields = line.split(/\s+/);
|
|
24
|
+
if (fields.length < 4) continue;
|
|
25
|
+
if (fields[3] !== TCP_LISTEN) continue;
|
|
26
|
+
const local = fields[1];
|
|
27
|
+
const colon = local.lastIndexOf(":");
|
|
28
|
+
if (colon === -1) continue;
|
|
29
|
+
const hexPort = local.slice(colon + 1);
|
|
30
|
+
if (!/^[0-9A-Fa-f]{1,4}$/.test(hexPort)) continue;
|
|
31
|
+
const port = parseInt(hexPort, 16);
|
|
32
|
+
if (port > 0) ports.add(port);
|
|
33
|
+
}
|
|
34
|
+
return ports;
|
|
35
|
+
}
|
|
36
|
+
async function probePortListeningOnce(executor, port) {
|
|
37
|
+
const readable = [];
|
|
38
|
+
for (const file of PROC_NET_TCP_FILES) {
|
|
39
|
+
try {
|
|
40
|
+
readable.push(await executor.exec(`cat ${file} 2>/dev/null`, { timeout: 5e3 }));
|
|
41
|
+
} catch {
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (readable.length === 0) return null;
|
|
45
|
+
return parseListeningPorts(readable.join("\n")).has(port);
|
|
46
|
+
}
|
|
47
|
+
function delay(ms) {
|
|
48
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
49
|
+
}
|
|
50
|
+
async function waitForPortListening(executor, port, opts = {}) {
|
|
51
|
+
const timeoutMs = opts.timeoutMs ?? 45e3;
|
|
52
|
+
const intervalMs = opts.intervalMs ?? 1e3;
|
|
53
|
+
const deadline = Date.now() + timeoutMs;
|
|
54
|
+
let anyConclusive = false;
|
|
55
|
+
try {
|
|
56
|
+
for (; ; ) {
|
|
57
|
+
const result = await probePortListeningOnce(executor, port);
|
|
58
|
+
if (result === true) return { listening: true, checked: true };
|
|
59
|
+
if (result === false) anyConclusive = true;
|
|
60
|
+
const remaining = deadline - Date.now();
|
|
61
|
+
if (remaining <= 0) break;
|
|
62
|
+
await delay(Math.min(intervalMs, remaining));
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
return { listening: false, checked: false };
|
|
66
|
+
}
|
|
67
|
+
return anyConclusive ? { listening: false, checked: true } : { listening: false, checked: false };
|
|
68
|
+
}
|
|
69
|
+
async function waitForPortFree(executor, port, opts = {}) {
|
|
70
|
+
const timeoutMs = opts.timeoutMs ?? 15e3;
|
|
71
|
+
const intervalMs = opts.intervalMs ?? 500;
|
|
72
|
+
const deadline = Date.now() + timeoutMs;
|
|
73
|
+
let anyConclusive = false;
|
|
74
|
+
try {
|
|
75
|
+
for (; ; ) {
|
|
76
|
+
const result = await probePortListeningOnce(executor, port);
|
|
77
|
+
if (result === false) return { free: true, checked: true };
|
|
78
|
+
if (result === true) anyConclusive = true;
|
|
79
|
+
const remaining = deadline - Date.now();
|
|
80
|
+
if (remaining <= 0) break;
|
|
81
|
+
await delay(Math.min(intervalMs, remaining));
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
return { free: false, checked: false };
|
|
85
|
+
}
|
|
86
|
+
return anyConclusive ? { free: false, checked: true } : { free: false, checked: false };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ../../packages/adapters/src/runtime/port-conflict.ts
|
|
90
|
+
async function resolveListenerOwner(executor, pid) {
|
|
91
|
+
const cgroup = (await tryExec(executor, `cat /proc/${pid}/cgroup 2>/dev/null`))?.trim();
|
|
92
|
+
if (!cgroup) return { cgroupVerified: false };
|
|
93
|
+
const evidence = { cgroupVerified: true, processCgroup: cgroup };
|
|
94
|
+
const containerId = containerIdFromCgroup(cgroup);
|
|
95
|
+
if (containerId) return { ...evidence, containerId };
|
|
96
|
+
const systemdUnit = cgroup?.match(/(?:^|\/)([^/\n]+\.service)(?:$|\n|\/)/m)?.[1]?.trim();
|
|
97
|
+
if (!systemdUnit || !/^[A-Za-z0-9@._:-]+\.service$/.test(systemdUnit)) {
|
|
98
|
+
return evidence;
|
|
99
|
+
}
|
|
100
|
+
const description = await tryExec(
|
|
101
|
+
executor,
|
|
102
|
+
`systemctl show ${shellQuote(systemdUnit)} --property=Description --value 2>/dev/null || true`
|
|
103
|
+
);
|
|
104
|
+
const deploymentId = managedDeploymentIdFromUnit(systemdUnit);
|
|
105
|
+
return {
|
|
106
|
+
...evidence,
|
|
107
|
+
systemdUnit,
|
|
108
|
+
systemdDescription: description?.trim() || void 0,
|
|
109
|
+
...deploymentId ? { deploymentId, isManagedDeployment: true } : { isManagedDeployment: false }
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async function resolveListener(executor, port) {
|
|
113
|
+
const out = await tryExec(
|
|
114
|
+
executor,
|
|
115
|
+
`ss -tlnp sport = :${port} 2>/dev/null | grep LISTEN || lsof -ti tcp:${port} -sTCP:LISTEN 2>/dev/null || true`
|
|
116
|
+
);
|
|
117
|
+
const listenerPid = (output) => {
|
|
118
|
+
if (!output) return null;
|
|
119
|
+
const ssMatch = output.match(/pid=(\d+)/);
|
|
120
|
+
const lsofMatch = !ssMatch ? output.match(/^\s*(\d+)\s*$/m) : null;
|
|
121
|
+
const parsed = ssMatch?.[1] ?? lsofMatch?.[1];
|
|
122
|
+
if (!parsed) return null;
|
|
123
|
+
const pid2 = Number.parseInt(parsed, 10);
|
|
124
|
+
return Number.isSafeInteger(pid2) && pid2 > 0 ? pid2 : null;
|
|
125
|
+
};
|
|
126
|
+
const pid = listenerPid(out);
|
|
127
|
+
if (pid) return { pid, occupied: true, checked: true };
|
|
128
|
+
const observedListener = Boolean(out?.trim());
|
|
129
|
+
if (observedListener) {
|
|
130
|
+
const privilegedOut = await tryExec(
|
|
131
|
+
executor,
|
|
132
|
+
`sudo -n ss -tlnp sport = :${port} 2>/dev/null | grep LISTEN || sudo -n lsof -ti tcp:${port} -sTCP:LISTEN 2>/dev/null || true`,
|
|
133
|
+
{ timeout: 5e3 }
|
|
134
|
+
);
|
|
135
|
+
const privilegedPid = listenerPid(privilegedOut);
|
|
136
|
+
if (privilegedPid) return { pid: privilegedPid, occupied: true, checked: true };
|
|
137
|
+
}
|
|
138
|
+
const listening = await probePortListeningOnce(executor, port);
|
|
139
|
+
return {
|
|
140
|
+
pid: null,
|
|
141
|
+
occupied: observedListener || listening === true,
|
|
142
|
+
checked: observedListener || listening !== null
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function parseListenerPids(output) {
|
|
146
|
+
const lines = (output ?? "").split("\n").map((line) => line.trim()).filter(Boolean);
|
|
147
|
+
if (lines.length === 0) return { observed: false, complete: true, pids: [] };
|
|
148
|
+
const pids = /* @__PURE__ */ new Set();
|
|
149
|
+
let complete = true;
|
|
150
|
+
for (const line of lines) {
|
|
151
|
+
const matches = [...line.matchAll(/pid=(\d+)/g)].map((match) => match[1]);
|
|
152
|
+
const bare = matches.length === 0 ? line.match(/^(\d+)$/)?.[1] : void 0;
|
|
153
|
+
const values = bare ? [bare] : matches;
|
|
154
|
+
if (values.length === 0) complete = false;
|
|
155
|
+
for (const value of values) {
|
|
156
|
+
const pid = Number.parseInt(value, 10);
|
|
157
|
+
if (Number.isSafeInteger(pid) && pid > 0) pids.add(pid);
|
|
158
|
+
else complete = false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { observed: true, complete, pids: [...pids] };
|
|
162
|
+
}
|
|
163
|
+
async function probeListeningPortOwners(executor, port) {
|
|
164
|
+
const command = `ss -tlnp sport = :${port} 2>/dev/null | grep LISTEN || lsof -ti tcp:${port} -sTCP:LISTEN 2>/dev/null || true`;
|
|
165
|
+
const ordinary = parseListenerPids(await tryExec(executor, command));
|
|
166
|
+
let evidence = ordinary;
|
|
167
|
+
if (ordinary.observed && !ordinary.complete) {
|
|
168
|
+
const privileged = parseListenerPids(
|
|
169
|
+
await tryExec(
|
|
170
|
+
executor,
|
|
171
|
+
`sudo -n ss -tlnp sport = :${port} 2>/dev/null | grep LISTEN || sudo -n lsof -ti tcp:${port} -sTCP:LISTEN 2>/dev/null`,
|
|
172
|
+
{ timeout: 5e3 }
|
|
173
|
+
)
|
|
174
|
+
);
|
|
175
|
+
if (privileged.observed && privileged.complete) evidence = privileged;
|
|
176
|
+
else {
|
|
177
|
+
evidence = {
|
|
178
|
+
observed: true,
|
|
179
|
+
complete: false,
|
|
180
|
+
pids: [.../* @__PURE__ */ new Set([...ordinary.pids, ...privileged.pids])]
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (!evidence.observed) {
|
|
185
|
+
const listening = await probePortListeningOnce(executor, port);
|
|
186
|
+
if (listening === null) return { occupants: [], checked: false, ownershipComplete: false };
|
|
187
|
+
if (!listening) return { occupants: [], checked: true, ownershipComplete: true };
|
|
188
|
+
return { occupants: [], checked: true, ownershipComplete: false };
|
|
189
|
+
}
|
|
190
|
+
const described = await Promise.all(
|
|
191
|
+
evidence.pids.map((pid) => describeProcess(executor, pid).catch(() => null))
|
|
192
|
+
);
|
|
193
|
+
const occupants = described.filter((row) => row !== null);
|
|
194
|
+
return {
|
|
195
|
+
occupants,
|
|
196
|
+
checked: true,
|
|
197
|
+
ownershipComplete: evidence.complete && occupants.length === evidence.pids.length
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
async function probeListeningPortState(executor, port) {
|
|
201
|
+
try {
|
|
202
|
+
const { pid, occupied, checked } = await resolveListener(executor, port);
|
|
203
|
+
if (!checked || !occupied) return { occupant: null, checked };
|
|
204
|
+
if (pid === null) {
|
|
205
|
+
return { occupant: { pid: null, command: "unknown listener" }, checked: true };
|
|
206
|
+
}
|
|
207
|
+
return { occupant: await describeProcess(executor, pid), checked: true };
|
|
208
|
+
} catch {
|
|
209
|
+
return { occupant: null, checked: false };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function probeListeningPort(executor, port) {
|
|
213
|
+
return (await probeListeningPortState(executor, port)).occupant;
|
|
214
|
+
}
|
|
215
|
+
async function describeProcess(executor, pid) {
|
|
216
|
+
let command = `PID ${pid}`;
|
|
217
|
+
let rawCommand;
|
|
218
|
+
const args = await tryExec(executor, `ps -p ${pid} -o args= 2>/dev/null || true`);
|
|
219
|
+
if (args?.trim()) {
|
|
220
|
+
rawCommand = args.trim();
|
|
221
|
+
command = `${rawCommand} (PID ${pid})`;
|
|
222
|
+
} else {
|
|
223
|
+
const cmd = await tryExec(executor, `ps -p ${pid} -o comm= 2>/dev/null || true`);
|
|
224
|
+
if (cmd?.trim()) {
|
|
225
|
+
rawCommand = cmd.trim();
|
|
226
|
+
command = `${rawCommand} (PID ${pid})`;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (isContainerPortForwarder(rawCommand)) {
|
|
230
|
+
return { pid, command, rawCommand, dockerPublished: true };
|
|
231
|
+
}
|
|
232
|
+
const systemd = await resolveListenerOwner(executor, pid);
|
|
233
|
+
return { pid, command, rawCommand, ...systemd };
|
|
234
|
+
}
|
|
235
|
+
function portOccupantDetails(port, occupant, stopTarget) {
|
|
236
|
+
if (!occupant) return { port };
|
|
237
|
+
return {
|
|
238
|
+
port,
|
|
239
|
+
pid: occupant.pid,
|
|
240
|
+
command: occupant.command,
|
|
241
|
+
rawCommand: occupant.rawCommand,
|
|
242
|
+
systemdUnit: occupant.systemdUnit,
|
|
243
|
+
systemdDescription: occupant.systemdDescription,
|
|
244
|
+
deploymentId: occupant.deploymentId,
|
|
245
|
+
isManagedDeployment: occupant.isManagedDeployment,
|
|
246
|
+
containerId: occupant.containerId,
|
|
247
|
+
containerName: occupant.containerName,
|
|
248
|
+
containerImage: occupant.containerImage,
|
|
249
|
+
ownerProjectId: occupant.ownerProjectId,
|
|
250
|
+
serviceName: occupant.serviceName,
|
|
251
|
+
dockerPublished: occupant.dockerPublished,
|
|
252
|
+
platformContainer: occupant.platformContainer,
|
|
253
|
+
ambiguousOwners: occupant.ambiguousOwners,
|
|
254
|
+
...stopTarget ? { stopTarget } : {}
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export {
|
|
259
|
+
waitForPortListening,
|
|
260
|
+
waitForPortFree,
|
|
261
|
+
probeListeningPortOwners,
|
|
262
|
+
probeListeningPort,
|
|
263
|
+
describeProcess,
|
|
264
|
+
portOccupantDetails
|
|
265
|
+
};
|
|
@@ -8,14 +8,14 @@ import {
|
|
|
8
8
|
} from "./chunk-I3DPMTSF.js";
|
|
9
9
|
import {
|
|
10
10
|
LocalExecutor
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-T6DVUX4F.js";
|
|
12
12
|
import {
|
|
13
13
|
logEntry,
|
|
14
14
|
sq
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-P7RCYELG.js";
|
|
16
16
|
import {
|
|
17
17
|
edgeFailureReason
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-X53JXRS4.js";
|
|
19
19
|
|
|
20
20
|
// ../../packages/adapters/src/system/docker-edge-executor.ts
|
|
21
21
|
import Dockerode from "dockerode";
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
":" //# ; exec "$(command -v node || command -v bun)" "$0" "$@"
|
|
3
|
+
import { createRequire as __ospCreateRequire } from "node:module";
|
|
4
|
+
const require = __ospCreateRequire(import.meta.url);
|
|
5
|
+
import {
|
|
6
|
+
formatDuration,
|
|
7
|
+
isRemoteConnectionError,
|
|
8
|
+
systemDebug
|
|
9
|
+
} from "./chunk-N5YZMO33.js";
|
|
10
|
+
import {
|
|
11
|
+
safeErrorMessage,
|
|
12
|
+
shellQuote
|
|
13
|
+
} from "./chunk-WEZFTL4V.js";
|
|
14
|
+
|
|
15
|
+
// ../../packages/adapters/src/system/probe-exec.ts
|
|
16
|
+
async function tryExec(executor, command, opts) {
|
|
17
|
+
try {
|
|
18
|
+
return opts ? await executor.exec(command, opts) : await executor.exec(command);
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function probeExec(executor, command, scope) {
|
|
24
|
+
const startedAt = Date.now();
|
|
25
|
+
systemDebug(scope, `exec:start ${command}`);
|
|
26
|
+
try {
|
|
27
|
+
const result = await executor.exec(command, { timeout: 1e4 });
|
|
28
|
+
systemDebug(scope, `exec:ok ${command} (${formatDuration(startedAt)})`);
|
|
29
|
+
return { output: result.trim(), error: null };
|
|
30
|
+
} catch (err) {
|
|
31
|
+
const msg = safeErrorMessage(err);
|
|
32
|
+
if (isRemoteConnectionError(err)) {
|
|
33
|
+
systemDebug(scope, `exec:abort ${command} (${formatDuration(startedAt)}) ${msg}`);
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
systemDebug(scope, `exec:fail ${command} (${formatDuration(startedAt)}) ${msg}`);
|
|
37
|
+
return { output: "", error: msg || `\`${command}\` failed` };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function withReason(message, error) {
|
|
41
|
+
if (!error) return message;
|
|
42
|
+
const first = error.split("\n").map((line) => line.trim()).find(Boolean);
|
|
43
|
+
if (!first) return message;
|
|
44
|
+
return `${message} \u2014 ${first.length > 200 ? `${first.slice(0, 199)}\u2026` : first}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ../../packages/adapters/src/infra/mail-container.ts
|
|
48
|
+
var MAIL_HOST_STATE_DIR = "/var/lib/openship/mail";
|
|
49
|
+
var MAIL_CONTAINER_MOUNTS = [
|
|
50
|
+
{ host: `${MAIL_HOST_STATE_DIR}/vmail`, container: "/var/vmail" },
|
|
51
|
+
// The Postfix queue. A recreate on an ephemeral layer would drop in-flight and
|
|
52
|
+
// deferred mail without this — keep it on the host.
|
|
53
|
+
{ host: `${MAIL_HOST_STATE_DIR}/spool`, container: "/var/spool/postfix" },
|
|
54
|
+
// DKIM private keys. NEVER baked, never regenerated on pull — a fresh selector
|
|
55
|
+
// breaks DMARC until DNS repropagates.
|
|
56
|
+
{ host: `${MAIL_HOST_STATE_DIR}/dkim`, container: "/var/lib/dkim" },
|
|
57
|
+
// Mutable daemon config the admin layer edits (sasl_passwd, relayhost maps,
|
|
58
|
+
// amavis 50-user). Seeded from baked defaults on first boot.
|
|
59
|
+
{ host: `${MAIL_HOST_STATE_DIR}/config/postfix`, container: "/etc/postfix", seed: true },
|
|
60
|
+
{ host: `${MAIL_HOST_STATE_DIR}/config/dovecot`, container: "/etc/dovecot", seed: true },
|
|
61
|
+
{ host: `${MAIL_HOST_STATE_DIR}/config/amavis`, container: "/etc/amavis/conf.d", seed: true },
|
|
62
|
+
// ClamAV signatures — bind-mounted so a pull doesn't force a multi-hundred-MB
|
|
63
|
+
// freshclam re-download and delay readiness. `seed: true` because that same mount
|
|
64
|
+
// HIDES the database baked into the image: with nothing copying it across, clamd
|
|
65
|
+
// finds an empty database directory and exits (issue #565).
|
|
66
|
+
{ host: `${MAIL_HOST_STATE_DIR}/clamav`, container: "/var/lib/clamav", seed: true },
|
|
67
|
+
{ host: "/etc/letsencrypt", container: "/etc/letsencrypt", readonly: true }
|
|
68
|
+
];
|
|
69
|
+
var MAIL_DB_HOST_DATA_DIR = `${MAIL_HOST_STATE_DIR}/pgdata`;
|
|
70
|
+
var MAIL_DB_CONTAINER_DATA_DIR = "/var/lib/postgresql/data";
|
|
71
|
+
var MAIL_DB_PGDATA = `${MAIL_DB_CONTAINER_DATA_DIR}/pgdata`;
|
|
72
|
+
var MAIL_HOST_PATHS = {
|
|
73
|
+
saslPasswd: `${MAIL_HOST_STATE_DIR}/config/postfix/sasl_passwd`,
|
|
74
|
+
senderRelayhost: `${MAIL_HOST_STATE_DIR}/config/postfix/sender_relayhost`,
|
|
75
|
+
// Per-nexthop TLS policy. Only split ("selected"-scope) relaying writes it: the
|
|
76
|
+
// relay hop must be `encrypt` so SASL credentials never cross a plaintext
|
|
77
|
+
// connection, while direct-to-MX delivery for the un-relayed domains stays
|
|
78
|
+
// opportunistic. A GLOBAL smtp_tls_security_level can't express both.
|
|
79
|
+
relayTlsPolicy: `${MAIL_HOST_STATE_DIR}/config/postfix/openship_tls_policy`,
|
|
80
|
+
amavisUserConf: `${MAIL_HOST_STATE_DIR}/config/amavis/50-user`
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// ../../packages/adapters/src/system/port-owner.ts
|
|
84
|
+
var OPENSHIP_LABEL = {
|
|
85
|
+
project: "openship.project",
|
|
86
|
+
deployment: "openship.deployment",
|
|
87
|
+
service: "openship.service",
|
|
88
|
+
build: "openship.build"
|
|
89
|
+
};
|
|
90
|
+
var PS_FIELDS = [
|
|
91
|
+
"{{.ID}}",
|
|
92
|
+
"{{.Names}}",
|
|
93
|
+
"{{.Image}}",
|
|
94
|
+
`{{.Label "${OPENSHIP_LABEL.project}"}}`,
|
|
95
|
+
`{{.Label "${OPENSHIP_LABEL.deployment}"}}`,
|
|
96
|
+
`{{.Label "${OPENSHIP_LABEL.service}"}}`,
|
|
97
|
+
`{{.Label "${OPENSHIP_LABEL.build}"}}`,
|
|
98
|
+
'{{.Label "com.docker.compose.project"}}',
|
|
99
|
+
'{{.Label "com.docker.compose.service"}}'
|
|
100
|
+
];
|
|
101
|
+
var PS_FORMAT = PS_FIELDS.join("\\t");
|
|
102
|
+
function parsePsLines(out) {
|
|
103
|
+
const containers = [];
|
|
104
|
+
for (const line of out.split("\n")) {
|
|
105
|
+
if (!line.trim()) continue;
|
|
106
|
+
const f = line.split(" ");
|
|
107
|
+
const id = f[0]?.trim();
|
|
108
|
+
const name = f[1]?.trim();
|
|
109
|
+
if (!id || !name || !/^[0-9a-f]{12,64}$/i.test(id)) continue;
|
|
110
|
+
containers.push({
|
|
111
|
+
id,
|
|
112
|
+
name: name.replace(/^\//, ""),
|
|
113
|
+
image: f[2]?.trim() ?? "",
|
|
114
|
+
...f[3]?.trim() ? { projectId: f[3].trim() } : {},
|
|
115
|
+
...f[4]?.trim() ? { deploymentId: f[4].trim() } : {},
|
|
116
|
+
...f[5]?.trim() ? { serviceName: f[5].trim() } : {},
|
|
117
|
+
...f[6]?.trim() ? { buildSessionId: f[6].trim() } : {},
|
|
118
|
+
...f[7]?.trim() ? { composeProject: f[7].trim() } : {},
|
|
119
|
+
...f[8]?.trim() ? { composeService: f[8].trim() } : {}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return containers;
|
|
123
|
+
}
|
|
124
|
+
async function queryContainers(executor, filter) {
|
|
125
|
+
const out = await tryExec(
|
|
126
|
+
executor,
|
|
127
|
+
`docker ps --no-trunc --filter ${filter} --format '${PS_FORMAT}' 2>/dev/null`
|
|
128
|
+
);
|
|
129
|
+
return out === null ? null : parsePsLines(out);
|
|
130
|
+
}
|
|
131
|
+
async function resolveContainerPublishingPort(executor, port) {
|
|
132
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) return { kind: "unknown" };
|
|
133
|
+
const containers = await queryContainers(executor, `publish=${port}`);
|
|
134
|
+
if (containers === null) return { kind: "unknown" };
|
|
135
|
+
if (containers.length === 0) return { kind: "none" };
|
|
136
|
+
if (containers.length === 1) return { kind: "one", container: containers[0] };
|
|
137
|
+
return { kind: "ambiguous", containers };
|
|
138
|
+
}
|
|
139
|
+
function containerIdFromCgroup(cgroup) {
|
|
140
|
+
const match = cgroup?.match(/(?:^|[^0-9a-f])([0-9a-f]{64})(?:[^0-9a-f]|$)/);
|
|
141
|
+
return match?.[1] ?? null;
|
|
142
|
+
}
|
|
143
|
+
async function stopContainerDurably(executor, ref) {
|
|
144
|
+
await tryExec(executor, `docker update --restart=no ${shellQuote(ref)} 2>/dev/null || true`);
|
|
145
|
+
await tryExec(executor, `docker stop ${shellQuote(ref)} 2>/dev/null || true`);
|
|
146
|
+
}
|
|
147
|
+
async function describeContainerById(executor, containerId) {
|
|
148
|
+
if (!/^[0-9a-f]{12,64}$/i.test(containerId)) return null;
|
|
149
|
+
const containers = await queryContainers(executor, `id=${shellQuote(containerId)}`);
|
|
150
|
+
return containers?.[0] ?? null;
|
|
151
|
+
}
|
|
152
|
+
var EDGE_CONTAINER_NAME = "openship-edge";
|
|
153
|
+
function isOurEdgeContainer(name, image) {
|
|
154
|
+
return /openship-edge/i.test(`${name ?? ""} ${image ?? ""}`);
|
|
155
|
+
}
|
|
156
|
+
var PORT_FORWARDER_NAMES = [
|
|
157
|
+
"docker-proxy",
|
|
158
|
+
"rootlesskit",
|
|
159
|
+
"rootlesskit-docker-proxy",
|
|
160
|
+
"slirp4netns",
|
|
161
|
+
"pasta",
|
|
162
|
+
// LXD/Incus publish a container port with `forkproxy`, whose cgroup is the snap's
|
|
163
|
+
// daemon unit — the same misattribution with a different daemon.
|
|
164
|
+
"forkproxy"
|
|
165
|
+
];
|
|
166
|
+
function isContainerPortForwarder(command) {
|
|
167
|
+
const argv0 = command?.trim().split(/\s+/)[0];
|
|
168
|
+
if (!argv0) return false;
|
|
169
|
+
const base = argv0.replace(/.*\//, "");
|
|
170
|
+
return PORT_FORWARDER_NAMES.some(
|
|
171
|
+
(name) => base === name || base.startsWith(`${name}-`) || base.length >= 15 && name.startsWith(base)
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
var MANAGED_DEPLOYMENT_UNIT_RE = /^openship-(dep_[A-Za-z0-9_-]+)\.service$/;
|
|
175
|
+
function managedDeploymentUnitName(deploymentId) {
|
|
176
|
+
return `openship-${deploymentId}.service`;
|
|
177
|
+
}
|
|
178
|
+
function managedDeploymentIdFromUnit(unit) {
|
|
179
|
+
return unit?.match(MANAGED_DEPLOYMENT_UNIT_RE)?.[1] ?? null;
|
|
180
|
+
}
|
|
181
|
+
var PROTECTED_UNITS = /* @__PURE__ */ new Set([
|
|
182
|
+
"docker",
|
|
183
|
+
"docker.socket",
|
|
184
|
+
"dockerd",
|
|
185
|
+
"snap.docker.dockerd",
|
|
186
|
+
"containerd",
|
|
187
|
+
"containerd.socket",
|
|
188
|
+
"podman",
|
|
189
|
+
// Other container/VM supervisors whose forwarders land on their daemon unit.
|
|
190
|
+
"lxd",
|
|
191
|
+
"snap.lxd.daemon",
|
|
192
|
+
"incus",
|
|
193
|
+
"k3s",
|
|
194
|
+
"k3s-agent",
|
|
195
|
+
"kubelet",
|
|
196
|
+
"libvirtd",
|
|
197
|
+
"ssh",
|
|
198
|
+
"sshd",
|
|
199
|
+
"dbus",
|
|
200
|
+
"systemd-resolved",
|
|
201
|
+
"systemd-networkd",
|
|
202
|
+
"systemd-logind",
|
|
203
|
+
// The control plane itself, and a from-source dev install of it.
|
|
204
|
+
"openship",
|
|
205
|
+
"openship-dev"
|
|
206
|
+
]);
|
|
207
|
+
var USER_MANAGER_UNIT_RE = /^user@\d+$/;
|
|
208
|
+
function classifyProxy(text) {
|
|
209
|
+
if (!text) return void 0;
|
|
210
|
+
const t = text.toLowerCase();
|
|
211
|
+
if (t.includes("openresty")) return "openresty";
|
|
212
|
+
if (/(^|[\s/:])nginx/.test(t)) return "nginx";
|
|
213
|
+
if (/(^|[\s/:])caddy/.test(t)) return "caddy";
|
|
214
|
+
if (/(apache2|httpd)/.test(t)) return "apache";
|
|
215
|
+
if (/(^|[\s/:])traefik/.test(t)) return "traefik";
|
|
216
|
+
if (/(^|[\s/:])haproxy/.test(t)) return "haproxy";
|
|
217
|
+
return void 0;
|
|
218
|
+
}
|
|
219
|
+
function bareUnit(unit) {
|
|
220
|
+
return unit.replace(/\.service$/, "");
|
|
221
|
+
}
|
|
222
|
+
function isProtectedUnit(unit) {
|
|
223
|
+
const bare = bareUnit(unit);
|
|
224
|
+
return PROTECTED_UNITS.has(bare) || USER_MANAGER_UNIT_RE.test(bare);
|
|
225
|
+
}
|
|
226
|
+
function mayStopUnitForEdgeTakeover(unit) {
|
|
227
|
+
if (!unit) return false;
|
|
228
|
+
if (managedDeploymentIdFromUnit(unit)) return true;
|
|
229
|
+
return !isProtectedUnit(unit);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export {
|
|
233
|
+
MAIL_HOST_STATE_DIR,
|
|
234
|
+
tryExec,
|
|
235
|
+
probeExec,
|
|
236
|
+
withReason,
|
|
237
|
+
OPENSHIP_LABEL,
|
|
238
|
+
resolveContainerPublishingPort,
|
|
239
|
+
containerIdFromCgroup,
|
|
240
|
+
stopContainerDurably,
|
|
241
|
+
describeContainerById,
|
|
242
|
+
EDGE_CONTAINER_NAME,
|
|
243
|
+
isOurEdgeContainer,
|
|
244
|
+
isContainerPortForwarder,
|
|
245
|
+
managedDeploymentUnitName,
|
|
246
|
+
managedDeploymentIdFromUnit,
|
|
247
|
+
classifyProxy,
|
|
248
|
+
mayStopUnitForEdgeTakeover
|
|
249
|
+
};
|