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
|
@@ -2,14 +2,6 @@
|
|
|
2
2
|
":" //# ; exec "$(command -v node || command -v bun)" "$0" "$@"
|
|
3
3
|
import { createRequire as __ospCreateRequire } from "node:module";
|
|
4
4
|
const require = __ospCreateRequire(import.meta.url);
|
|
5
|
-
import {
|
|
6
|
-
resolveEnvironment
|
|
7
|
-
} from "./chunk-IW2UKBVF.js";
|
|
8
|
-
import {
|
|
9
|
-
formatDuration,
|
|
10
|
-
isRemoteConnectionError,
|
|
11
|
-
systemDebug
|
|
12
|
-
} from "./chunk-ZCFVHA25.js";
|
|
13
5
|
import {
|
|
14
6
|
containerCommand,
|
|
15
7
|
edgeContainerExecutor,
|
|
@@ -17,7 +9,28 @@ import {
|
|
|
17
9
|
readDeclaredPair,
|
|
18
10
|
readMaybeInContainer,
|
|
19
11
|
validateCertFor
|
|
20
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-QVW76WDL.js";
|
|
13
|
+
import {
|
|
14
|
+
EDGE_CHALLENGE_HOST_DIR,
|
|
15
|
+
EDGE_CONTAINER_MOUNTS,
|
|
16
|
+
EDGE_HOST_PATHS,
|
|
17
|
+
OPENRESTY_DEFAULT_PATHS
|
|
18
|
+
} from "./chunk-RSR7PFIQ.js";
|
|
19
|
+
import {
|
|
20
|
+
privilegedExecutor,
|
|
21
|
+
rootChecked,
|
|
22
|
+
rootOrDegrade
|
|
23
|
+
} from "./chunk-4I2BM2DT.js";
|
|
24
|
+
import {
|
|
25
|
+
envOps,
|
|
26
|
+
opScript
|
|
27
|
+
} from "./chunk-YMMQRE5J.js";
|
|
28
|
+
import {
|
|
29
|
+
resolveEnvironment
|
|
30
|
+
} from "./chunk-UVNMQM7W.js";
|
|
31
|
+
import {
|
|
32
|
+
sq
|
|
33
|
+
} from "./chunk-ZFJXPCQP.js";
|
|
21
34
|
import {
|
|
22
35
|
EDGE_CONTAINER_NAME,
|
|
23
36
|
EdgeConflictError,
|
|
@@ -32,28 +45,25 @@ import {
|
|
|
32
45
|
sanitizeEdgeVhosts,
|
|
33
46
|
sq as sq2,
|
|
34
47
|
stopTargetsForStatus
|
|
35
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-2YM3NI2E.js";
|
|
36
49
|
import {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
EDGE_HOST_PATHS,
|
|
43
|
-
OPENRESTY_DEFAULT_PATHS
|
|
44
|
-
} from "./chunk-YIPMDGG6.js";
|
|
45
|
-
import {
|
|
46
|
-
sq
|
|
47
|
-
} from "./chunk-ZFJXPCQP.js";
|
|
50
|
+
probeExec,
|
|
51
|
+
tryExec,
|
|
52
|
+
waitForPortListening,
|
|
53
|
+
withReason
|
|
54
|
+
} from "./chunk-SM26HDS2.js";
|
|
48
55
|
import {
|
|
49
56
|
AppError,
|
|
50
57
|
PROXY_DIRECTIVES,
|
|
58
|
+
answered,
|
|
51
59
|
buildEdgeImageRef,
|
|
52
60
|
compareSemver,
|
|
61
|
+
formatDuration,
|
|
53
62
|
parseProxyValue,
|
|
54
63
|
safeErrorMessage,
|
|
55
|
-
sanitizeProxySettings
|
|
56
|
-
|
|
64
|
+
sanitizeProxySettings,
|
|
65
|
+
systemDebug
|
|
66
|
+
} from "./chunk-UJ6JZM65.js";
|
|
57
67
|
|
|
58
68
|
// ../../packages/adapters/src/system/managed-image.ts
|
|
59
69
|
import { posix } from "path";
|
|
@@ -120,14 +130,7 @@ var JOURNAL_PATH = `${JOURNAL_DIR}/edge-takeover.json`;
|
|
|
120
130
|
function log2(message, level = "info") {
|
|
121
131
|
return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
|
|
122
132
|
}
|
|
123
|
-
async function
|
|
124
|
-
try {
|
|
125
|
-
return await executor.exec(cmd);
|
|
126
|
-
} catch {
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
async function buildJournal(executor, status) {
|
|
133
|
+
async function buildJournal(executor, status, onLog) {
|
|
131
134
|
const units = /* @__PURE__ */ new Map();
|
|
132
135
|
const containers = /* @__PURE__ */ new Map();
|
|
133
136
|
const processes = /* @__PURE__ */ new Map();
|
|
@@ -137,7 +140,17 @@ async function buildJournal(executor, status) {
|
|
|
137
140
|
executor,
|
|
138
141
|
`docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' ${sq2(o.containerName)} 2>/dev/null`
|
|
139
142
|
);
|
|
140
|
-
|
|
143
|
+
if (r === null) {
|
|
144
|
+
onLog?.(
|
|
145
|
+
log2(
|
|
146
|
+
`Could not read ${o.containerName}'s restart policy. If this takeover is rolled back it will be started again, but its policy left untouched \u2014 check it with \`docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' ${o.containerName}\`.`,
|
|
147
|
+
"warn"
|
|
148
|
+
)
|
|
149
|
+
);
|
|
150
|
+
containers.set(o.containerName, { name: o.containerName });
|
|
151
|
+
} else {
|
|
152
|
+
containers.set(o.containerName, { name: o.containerName, restart: r.trim() || "no" });
|
|
153
|
+
}
|
|
141
154
|
} else if (o.systemdUnit && !units.has(o.systemdUnit)) {
|
|
142
155
|
const en = await tryExec(executor, `systemctl is-enabled ${sq2(o.systemdUnit)} 2>/dev/null`);
|
|
143
156
|
units.set(o.systemdUnit, { unit: o.systemdUnit, wasEnabled: en?.trim() === "enabled" });
|
|
@@ -152,19 +165,44 @@ async function buildJournal(executor, status) {
|
|
|
152
165
|
processes: [...processes.values()]
|
|
153
166
|
};
|
|
154
167
|
}
|
|
168
|
+
function journalFileExecutor(executor) {
|
|
169
|
+
return rootOrDegrade(executor, {
|
|
170
|
+
purpose: "Writing the edge-takeover rollback journal",
|
|
171
|
+
consequence: "An interrupted takeover won't be repaired automatically on the next boot.",
|
|
172
|
+
report: () => {
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
155
176
|
async function writeJournal(executor, journal) {
|
|
156
177
|
try {
|
|
157
|
-
await executor
|
|
158
|
-
await
|
|
178
|
+
const privileged = await journalFileExecutor(executor);
|
|
179
|
+
await privileged.mkdir(JOURNAL_DIR);
|
|
180
|
+
await privileged.writeFile(JOURNAL_PATH, JSON.stringify(journal, null, 2));
|
|
159
181
|
} catch {
|
|
160
182
|
}
|
|
161
183
|
}
|
|
162
184
|
async function clearJournal(executor) {
|
|
163
|
-
await tryExec(executor, `rm -f ${JOURNAL_PATH}`);
|
|
164
|
-
}
|
|
165
|
-
async function readJournal(executor) {
|
|
166
|
-
const
|
|
167
|
-
|
|
185
|
+
await tryExec(await journalFileExecutor(executor), `rm -f ${JOURNAL_PATH}`);
|
|
186
|
+
}
|
|
187
|
+
async function readJournal(executor, onLog) {
|
|
188
|
+
const privileged = await journalFileExecutor(executor);
|
|
189
|
+
const raw = await tryExec(privileged, `cat ${JOURNAL_PATH} 2>/dev/null`);
|
|
190
|
+
if (raw === null) {
|
|
191
|
+
const listed = await tryExec(privileged, `ls ${JOURNAL_PATH} 2>/dev/null`);
|
|
192
|
+
if (listed?.trim()) {
|
|
193
|
+
onLog?.(
|
|
194
|
+
log2(
|
|
195
|
+
`An unfinished edge takeover is recorded at ${JOURNAL_PATH}, but Openship could not read it as this user \u2014 the previous proxy will NOT be restored automatically. Re-run as root, or restore it by hand.`,
|
|
196
|
+
"error"
|
|
197
|
+
)
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
if (!raw.trim()) {
|
|
203
|
+
await clearJournal(executor);
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
168
206
|
try {
|
|
169
207
|
return JSON.parse(raw);
|
|
170
208
|
} catch {
|
|
@@ -174,26 +212,43 @@ async function readJournal(executor) {
|
|
|
174
212
|
}
|
|
175
213
|
async function rollback(executor, journal, onLog) {
|
|
176
214
|
onLog(log2("Rolling back \u2014 restoring the previous proxy...", "warn"));
|
|
177
|
-
await
|
|
178
|
-
|
|
215
|
+
const restore = await rootOrDegrade(executor, {
|
|
216
|
+
purpose: "Restarting the proxy that was holding ports 80/443",
|
|
217
|
+
consequence: "Restarting and re-enabling it will likely be refused \u2014 so if :80 is served after this, it is because nothing was ever stopped, not because Openship put it back.",
|
|
218
|
+
report: (message) => onLog(log2(message, "warn"))
|
|
219
|
+
});
|
|
220
|
+
await tryExec(restore, `docker update --restart=no ${sq2(EDGE_CONTAINER_NAME)} 2>/dev/null || true`);
|
|
221
|
+
await tryExec(restore, `docker stop ${sq2(EDGE_CONTAINER_NAME)} 2>/dev/null || true`);
|
|
179
222
|
await tryExec(
|
|
180
|
-
|
|
223
|
+
restore,
|
|
181
224
|
"systemctl disable --now openresty 2>/dev/null || systemctl stop openresty 2>/dev/null || true; systemctl reset-failed openresty 2>/dev/null || true"
|
|
182
225
|
);
|
|
183
226
|
for (const u of journal.units) {
|
|
184
227
|
await tryExec(
|
|
185
|
-
|
|
228
|
+
restore,
|
|
186
229
|
u.wasEnabled ? `systemctl enable --now ${sq2(u.unit)} 2>/dev/null || true` : `systemctl start ${sq2(u.unit)} 2>/dev/null || true`
|
|
187
230
|
);
|
|
188
231
|
}
|
|
189
232
|
for (const c of journal.containers) {
|
|
190
|
-
|
|
191
|
-
|
|
233
|
+
if (c.restart) {
|
|
234
|
+
await tryExec(
|
|
235
|
+
restore,
|
|
236
|
+
`docker update --restart=${sq2(c.restart)} ${sq2(c.name)} 2>/dev/null || true`
|
|
237
|
+
);
|
|
238
|
+
} else {
|
|
239
|
+
onLog(
|
|
240
|
+
log2(
|
|
241
|
+
`Starting ${c.name} without setting a restart policy \u2014 Openship never managed to read the one it had. Confirm it still comes back on reboot.`,
|
|
242
|
+
"warn"
|
|
243
|
+
)
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
await tryExec(restore, `docker start ${sq2(c.name)} 2>/dev/null || true`);
|
|
192
247
|
}
|
|
193
248
|
for (const p of journal.processes ?? []) {
|
|
194
249
|
if (p.command) {
|
|
195
250
|
await tryExec(
|
|
196
|
-
|
|
251
|
+
restore,
|
|
197
252
|
`setsid -f sh -c ${sq2(p.command)} 2>/dev/null || (nohup sh -c ${sq2(p.command)} >/dev/null 2>&1 &) || true`
|
|
198
253
|
);
|
|
199
254
|
} else {
|
|
@@ -219,16 +274,29 @@ async function portIsServed(executor, port) {
|
|
|
219
274
|
);
|
|
220
275
|
return (out ?? "").includes("yes");
|
|
221
276
|
}
|
|
277
|
+
var EDGE_STOP_ELEVATION = {
|
|
278
|
+
purpose: "Stopping the proxy that holds ports 80/443",
|
|
279
|
+
consequence: "Stopping and disabling it will likely be refused, leaving the ports bound."
|
|
280
|
+
};
|
|
222
281
|
async function beginEdgeTakeover(executor, status, onLog, targets) {
|
|
223
|
-
await writeJournal(executor, await buildJournal(executor, status));
|
|
224
|
-
|
|
225
|
-
|
|
282
|
+
await writeJournal(executor, await buildJournal(executor, status, onLog));
|
|
283
|
+
let privilegeDegraded = false;
|
|
284
|
+
const stopExecutor = await rootOrDegrade(executor, {
|
|
285
|
+
...EDGE_STOP_ELEVATION,
|
|
286
|
+
report: (message) => {
|
|
287
|
+
privilegeDegraded = true;
|
|
288
|
+
onLog(log2(message, "warn"));
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
const freed = await freeEdgeTargets(
|
|
292
|
+
stopExecutor,
|
|
226
293
|
targets?.length ? targets : stopTargetsForStatus(status),
|
|
227
294
|
(m, l) => onLog(log2(m, l))
|
|
228
295
|
);
|
|
296
|
+
return { ...freed, privilegeDegraded };
|
|
229
297
|
}
|
|
230
298
|
async function rollbackEdgeTakeover(executor, onLog) {
|
|
231
|
-
const journal = await readJournal(executor);
|
|
299
|
+
const journal = await readJournal(executor, onLog);
|
|
232
300
|
if (!journal || journal.completed) return false;
|
|
233
301
|
const restored = await rollback(executor, journal, onLog);
|
|
234
302
|
await clearJournal(executor);
|
|
@@ -243,7 +311,7 @@ async function completeEdgeTakeover(executor) {
|
|
|
243
311
|
await clearJournal(executor);
|
|
244
312
|
}
|
|
245
313
|
async function recoverInterruptedTakeover(executor, onLog, isEdgeHealthy) {
|
|
246
|
-
const journal = await readJournal(executor);
|
|
314
|
+
const journal = await readJournal(executor, onLog);
|
|
247
315
|
if (!journal) return;
|
|
248
316
|
if (journal.completed) {
|
|
249
317
|
await clearJournal(executor);
|
|
@@ -260,13 +328,6 @@ async function recoverInterruptedTakeover(executor, onLog, isEdgeHealthy) {
|
|
|
260
328
|
}
|
|
261
329
|
|
|
262
330
|
// ../../packages/adapters/src/system/proxy/import/parse-utils.ts
|
|
263
|
-
async function tryExec2(executor, command) {
|
|
264
|
-
try {
|
|
265
|
-
return await executor.exec(command);
|
|
266
|
-
} catch {
|
|
267
|
-
return null;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
331
|
function stripComments(text) {
|
|
271
332
|
return text.split("\n").map((line) => {
|
|
272
333
|
let quote;
|
|
@@ -330,15 +391,23 @@ function sq3(value) {
|
|
|
330
391
|
// ../../packages/adapters/src/system/proxy/import/nginx.ts
|
|
331
392
|
async function dumpResolvedConfig(executor, bins) {
|
|
332
393
|
for (const bin of bins) {
|
|
333
|
-
const dumped = await
|
|
394
|
+
const dumped = await tryExec(executor, `${bin} -T 2>/dev/null`);
|
|
334
395
|
if (dumped && /server\s*\{/.test(dumped)) return dumped;
|
|
335
396
|
}
|
|
336
397
|
return null;
|
|
337
398
|
}
|
|
399
|
+
async function nginxPrefix(executor, bins) {
|
|
400
|
+
for (const bin of bins) {
|
|
401
|
+
const out = await tryExec(executor, `${bin} -V 2>&1`);
|
|
402
|
+
const prefix = out?.match(/--prefix=(\S+)/)?.[1];
|
|
403
|
+
if (prefix) return prefix;
|
|
404
|
+
}
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
338
407
|
async function loadNginxConfig(executor) {
|
|
339
408
|
const dumped = await dumpResolvedConfig(executor, ["nginx", "openresty"]);
|
|
340
409
|
if (dumped) return dumped;
|
|
341
|
-
const cat = await
|
|
410
|
+
const cat = await tryExec(
|
|
342
411
|
executor,
|
|
343
412
|
"cat /etc/nginx/nginx.conf /etc/nginx/sites-enabled/* /etc/nginx/conf.d/*.conf 2>/dev/null"
|
|
344
413
|
);
|
|
@@ -450,9 +519,24 @@ function parseProxyDirectives(body) {
|
|
|
450
519
|
...Object.keys(raw).length > 0 ? { raw } : {}
|
|
451
520
|
};
|
|
452
521
|
}
|
|
453
|
-
|
|
522
|
+
var LOOPBACK_SERVER_NAMES = /* @__PURE__ */ new Set([
|
|
523
|
+
"localhost",
|
|
524
|
+
"localhost.localdomain",
|
|
525
|
+
"ip6-localhost",
|
|
526
|
+
"ip6-loopback"
|
|
527
|
+
]);
|
|
528
|
+
function resolveStaticRoot(root, prefix) {
|
|
529
|
+
if (root.startsWith("/")) return { root };
|
|
530
|
+
if (!prefix) {
|
|
531
|
+
return {
|
|
532
|
+
reason: `static root "${root}" is relative to nginx's compiled prefix, which this host didn't report`
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
return { root: `${prefix.replace(/\/+$/, "")}/${root}` };
|
|
536
|
+
}
|
|
537
|
+
function parseServer(body, source, upstreams, prefix) {
|
|
454
538
|
const warnings = [];
|
|
455
|
-
const names = firstDirective(body, "server_name")?.split(/\s+/).filter((n) => n && n !== "_" && !n.startsWith("~")) ?? [];
|
|
539
|
+
const names = firstDirective(body, "server_name")?.split(/\s+/).filter((n) => n && n !== "_" && !n.startsWith("~") && !LOOPBACK_SERVER_NAMES.has(n.toLowerCase())) ?? [];
|
|
456
540
|
const listens = [...body.matchAll(/(?:^|[;{\s])listen\s+([^;]+);/g)].map((m) => m[1]);
|
|
457
541
|
const ssl = listens.some((l) => /\bssl\b/.test(l) || /\b443\b/.test(l));
|
|
458
542
|
const root = firstDirective(body, "root");
|
|
@@ -479,7 +563,12 @@ function parseServer(body, source, upstreams) {
|
|
|
479
563
|
} else if (isHttpsUpgradeForSelf(body, names)) {
|
|
480
564
|
return { warnings: [] };
|
|
481
565
|
} else if (root && !isAcmeWebrootOnly(body)) {
|
|
482
|
-
|
|
566
|
+
const resolved2 = resolveStaticRoot(root.replace(/;$/, ""), prefix);
|
|
567
|
+
if ("reason" in resolved2) {
|
|
568
|
+
warnings.push(`nginx: ${names[0]} \u2014 ${resolved2.reason} (skipped)`);
|
|
569
|
+
return { warnings };
|
|
570
|
+
}
|
|
571
|
+
target = { kind: "static", root: resolved2.root };
|
|
483
572
|
} else if (root) {
|
|
484
573
|
return { warnings: [] };
|
|
485
574
|
} else {
|
|
@@ -494,7 +583,7 @@ function parseServer(body, source, upstreams) {
|
|
|
494
583
|
if (tunables.raw) site.proxyRaw = tunables.raw;
|
|
495
584
|
return { site, warnings };
|
|
496
585
|
}
|
|
497
|
-
function parseNginxConfig(raw) {
|
|
586
|
+
function parseNginxConfig(raw, prefix) {
|
|
498
587
|
const warnings = [];
|
|
499
588
|
const sites = [];
|
|
500
589
|
if (!raw.trim()) {
|
|
@@ -507,7 +596,7 @@ function parseNginxConfig(raw) {
|
|
|
507
596
|
warnings.push("nginx: no server blocks found");
|
|
508
597
|
}
|
|
509
598
|
for (const body of blocks) {
|
|
510
|
-
const { site, warnings: blockWarnings } = parseServer(body, "nginx", upstreams);
|
|
599
|
+
const { site, warnings: blockWarnings } = parseServer(body, "nginx", upstreams, prefix);
|
|
511
600
|
warnings.push(...blockWarnings);
|
|
512
601
|
if (site) sites.push(site);
|
|
513
602
|
}
|
|
@@ -519,7 +608,10 @@ function parseNginxConfig(raw) {
|
|
|
519
608
|
return { proxy: "nginx", sites: kept, warnings };
|
|
520
609
|
}
|
|
521
610
|
async function scanNginx(executor) {
|
|
522
|
-
|
|
611
|
+
const raw = await loadNginxConfig(executor);
|
|
612
|
+
const relativeRoot = /(?:^|[;{\s])root\s+[^/;\s]/.test(raw);
|
|
613
|
+
const prefix = relativeRoot ? await nginxPrefix(executor, ["nginx", "openresty"]) : null;
|
|
614
|
+
return parseNginxConfig(raw, prefix ?? void 0);
|
|
523
615
|
}
|
|
524
616
|
var OUR_EDGE_SITE_GLOBS = [
|
|
525
617
|
`${EDGE_HOST_PATHS.sitesDir}/*.conf`,
|
|
@@ -533,11 +625,11 @@ async function scanForeignOpenResty(executor) {
|
|
|
533
625
|
}
|
|
534
626
|
async function scanOpenshipEdge(executor) {
|
|
535
627
|
const cat = `cat ${OUR_EDGE_SITE_GLOBS.join(" ")} 2>/dev/null`;
|
|
536
|
-
const raw = await
|
|
628
|
+
const raw = await tryExec(executor, cat);
|
|
537
629
|
if (raw?.trim()) return parseNginxConfig(raw);
|
|
538
630
|
const container = await resolveOurEdgeContainer(executor);
|
|
539
631
|
if (!container) return parseNginxConfig("");
|
|
540
|
-
const fromContainer = await
|
|
632
|
+
const fromContainer = await tryExec(executor, containerCommand(container, cat));
|
|
541
633
|
return parseNginxConfig(fromContainer ?? "");
|
|
542
634
|
}
|
|
543
635
|
|
|
@@ -545,12 +637,12 @@ async function scanOpenshipEdge(executor) {
|
|
|
545
637
|
var CADDYFILE_PATHS = ["/etc/caddy/Caddyfile", "/etc/Caddyfile"];
|
|
546
638
|
async function loadCaddyAdminConfig(executor) {
|
|
547
639
|
const endpoint = "http://localhost:2019/config/";
|
|
548
|
-
const out = await
|
|
640
|
+
const out = await tryExec(executor, `curl -fsS ${endpoint} 2>/dev/null`) || await tryExec(executor, `wget -qO- ${endpoint} 2>/dev/null`);
|
|
549
641
|
return out && out.trim().startsWith("{") ? out : null;
|
|
550
642
|
}
|
|
551
643
|
async function loadCaddyJson(executor) {
|
|
552
644
|
for (const p of CADDYFILE_PATHS) {
|
|
553
|
-
const out = await
|
|
645
|
+
const out = await tryExec(executor, `caddy adapt --adapter caddyfile --config ${p} 2>/dev/null`);
|
|
554
646
|
if (out && out.trim().startsWith("{")) return out;
|
|
555
647
|
}
|
|
556
648
|
return null;
|
|
@@ -665,7 +757,7 @@ function parseCaddyJson(raw) {
|
|
|
665
757
|
}
|
|
666
758
|
async function loadCaddyfile(executor) {
|
|
667
759
|
for (const p of CADDYFILE_PATHS) {
|
|
668
|
-
const out = await
|
|
760
|
+
const out = await tryExec(executor, `cat ${p} 2>/dev/null`);
|
|
669
761
|
if (out && out.trim()) return out;
|
|
670
762
|
}
|
|
671
763
|
return "";
|
|
@@ -800,7 +892,7 @@ async function scanCaddyText(executor) {
|
|
|
800
892
|
|
|
801
893
|
// ../../packages/adapters/src/system/proxy/import/apache.ts
|
|
802
894
|
async function vhostFilesFromDump(executor) {
|
|
803
|
-
const dump = await
|
|
895
|
+
const dump = await tryExec(
|
|
804
896
|
executor,
|
|
805
897
|
"apachectl -S 2>/dev/null || apache2ctl -S 2>/dev/null || httpd -S 2>/dev/null"
|
|
806
898
|
);
|
|
@@ -815,10 +907,10 @@ async function vhostFilesFromDump(executor) {
|
|
|
815
907
|
async function loadApacheConfig(executor) {
|
|
816
908
|
const files = await vhostFilesFromDump(executor);
|
|
817
909
|
if (files.length > 0) {
|
|
818
|
-
const cat2 = await
|
|
910
|
+
const cat2 = await tryExec(executor, `cat ${files.map(sq3).join(" ")} 2>/dev/null`);
|
|
819
911
|
if (cat2 && cat2.trim()) return cat2;
|
|
820
912
|
}
|
|
821
|
-
const cat = await
|
|
913
|
+
const cat = await tryExec(
|
|
822
914
|
executor,
|
|
823
915
|
"cat /etc/apache2/sites-enabled/*.conf /etc/apache2/apache2.conf /etc/httpd/conf.d/*.conf /etc/httpd/conf/httpd.conf 2>/dev/null"
|
|
824
916
|
);
|
|
@@ -1145,7 +1237,7 @@ function parseTraefikLabels(containers) {
|
|
|
1145
1237
|
};
|
|
1146
1238
|
}
|
|
1147
1239
|
async function scanTraefik(executor) {
|
|
1148
|
-
const out = await
|
|
1240
|
+
const out = await tryExec(
|
|
1149
1241
|
executor,
|
|
1150
1242
|
`docker ps -q 2>/dev/null | xargs -r docker inspect --format '{{.Name}} {{json .Config.Labels}} {{range $n, $v := .NetworkSettings.Networks}}{{$n}}={{$v.IPAddress}} {{end}} {{range $p, $v := .Config.ExposedPorts}}{{$p}} {{end}} {{if .Config.Cmd}}{{join .Config.Cmd " "}}{{end}}' 2>/dev/null`
|
|
1151
1243
|
);
|
|
@@ -1277,10 +1369,10 @@ function candidateNames(host) {
|
|
|
1277
1369
|
async function findPairIn(exec, root, host, container) {
|
|
1278
1370
|
for (const name of candidateNames(host)) {
|
|
1279
1371
|
const glob = `${root}/certificates/*/${name}/${name}.crt`;
|
|
1280
|
-
let certPath = firstCrt(await
|
|
1372
|
+
let certPath = firstCrt(await tryExec(exec, `ls -1 ${glob} 2>/dev/null`));
|
|
1281
1373
|
if (!certPath && container) {
|
|
1282
1374
|
certPath = firstCrt(
|
|
1283
|
-
await
|
|
1375
|
+
await tryExec(exec, containerCommand(container, `ls -1 ${glob} 2>/dev/null`))
|
|
1284
1376
|
);
|
|
1285
1377
|
}
|
|
1286
1378
|
if (certPath) return { certPath, keyPath: certPath.replace(/\.crt$/, ".key") };
|
|
@@ -1326,7 +1418,7 @@ async function candidateStoragePaths(exec, container) {
|
|
|
1326
1418
|
};
|
|
1327
1419
|
if (container) {
|
|
1328
1420
|
collect(
|
|
1329
|
-
await
|
|
1421
|
+
await tryExec(
|
|
1330
1422
|
exec,
|
|
1331
1423
|
`docker inspect ${sq2(container)} --format '{{json .Config.Cmd}} {{json .Args}} {{json .Config.Env}}' 2>/dev/null`
|
|
1332
1424
|
)
|
|
@@ -1558,61 +1650,21 @@ function makeApi(exec, kind, ours, container) {
|
|
|
1558
1650
|
}
|
|
1559
1651
|
|
|
1560
1652
|
// ../../packages/adapters/src/system/catalog.ts
|
|
1653
|
+
var MIN_DOCKER_VERSION = "20.10";
|
|
1561
1654
|
function dockerInstallPlan(profile) {
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
supported: true,
|
|
1570
|
-
installCommand: "curl -fsSL https://get.docker.com | sh",
|
|
1571
|
-
startCommand: profile.serviceManager === "systemd" ? "systemctl enable docker && systemctl start docker" : void 0,
|
|
1655
|
+
const ops = envOps(profile);
|
|
1656
|
+
const install = ops.dockerInstall();
|
|
1657
|
+
if (!install.supported) return install;
|
|
1658
|
+
const start = ops.dockerStart();
|
|
1659
|
+
return answered({
|
|
1660
|
+
installCommand: opScript(install.value),
|
|
1661
|
+
service: start.supported ? answered(opScript(start.value)) : start,
|
|
1572
1662
|
verifyCommand: "docker --version"
|
|
1573
|
-
};
|
|
1574
|
-
}
|
|
1575
|
-
function gitInstallPlan(profile) {
|
|
1576
|
-
const commands = {
|
|
1577
|
-
apt: "apt-get update -qq && apt-get install -y -qq git",
|
|
1578
|
-
dnf: "dnf install -y git",
|
|
1579
|
-
yum: "yum install -y git",
|
|
1580
|
-
apk: "apk add --no-cache git",
|
|
1581
|
-
brew: "brew install git"
|
|
1582
|
-
};
|
|
1583
|
-
const installCommand = commands[profile.packageManager];
|
|
1584
|
-
if (!installCommand) {
|
|
1585
|
-
return {
|
|
1586
|
-
supported: false,
|
|
1587
|
-
unsupportedReason: "No supported package manager found for Git installation"
|
|
1588
|
-
};
|
|
1589
|
-
}
|
|
1590
|
-
return {
|
|
1591
|
-
supported: true,
|
|
1592
|
-
installCommand,
|
|
1593
|
-
verifyCommand: "git --version"
|
|
1594
|
-
};
|
|
1663
|
+
});
|
|
1595
1664
|
}
|
|
1596
|
-
function
|
|
1597
|
-
const
|
|
1598
|
-
|
|
1599
|
-
dnf: "dnf install -y rsync",
|
|
1600
|
-
yum: "yum install -y rsync",
|
|
1601
|
-
apk: "apk add --no-cache rsync",
|
|
1602
|
-
brew: "brew install rsync"
|
|
1603
|
-
};
|
|
1604
|
-
const installCommand = commands[profile.packageManager];
|
|
1605
|
-
if (!installCommand) {
|
|
1606
|
-
return {
|
|
1607
|
-
supported: false,
|
|
1608
|
-
unsupportedReason: "No supported package manager found for rsync installation"
|
|
1609
|
-
};
|
|
1610
|
-
}
|
|
1611
|
-
return {
|
|
1612
|
-
supported: true,
|
|
1613
|
-
installCommand,
|
|
1614
|
-
verifyCommand: "rsync --version | head -n 1"
|
|
1615
|
-
};
|
|
1665
|
+
function packagePlan(profile, pkg, verifyCommand) {
|
|
1666
|
+
const install = envOps(profile).pkgInstall([pkg]);
|
|
1667
|
+
return install.supported ? answered({ installCommand: opScript(install.value), verifyCommand }) : install;
|
|
1616
1668
|
}
|
|
1617
1669
|
var systemCatalog = {
|
|
1618
1670
|
checks: {
|
|
@@ -1626,7 +1678,11 @@ var systemCatalog = {
|
|
|
1626
1678
|
daemonCommand: "docker info --format '{{.ServerVersion}}'",
|
|
1627
1679
|
parseVersion: (output) => output.match(/Docker version ([^\s,]+)/)?.[1] ?? output,
|
|
1628
1680
|
missingMessage: "Docker is not installed",
|
|
1629
|
-
notRunningMessage: "Docker is installed but the daemon is not running"
|
|
1681
|
+
notRunningMessage: "Docker is installed but the daemon is not running",
|
|
1682
|
+
// Deliberately does NOT claim the daemon is up or down: the socket's permission
|
|
1683
|
+
// is checked before the daemon is asked, so this arm proves only that we may not
|
|
1684
|
+
// ask. What it can state is the fix, which is the part "not running" got wrong.
|
|
1685
|
+
deniedMessage: "Docker is installed but this connection is not allowed to use its socket \u2014 add the login user to the `docker` group, or connect as root"
|
|
1630
1686
|
},
|
|
1631
1687
|
openresty: {
|
|
1632
1688
|
versionCommand: "openresty -v 2>&1 || /usr/local/openresty/bin/openresty -v 2>&1",
|
|
@@ -1660,185 +1716,9 @@ var systemCatalog = {
|
|
|
1660
1716
|
// CONTAINER. See installer.ts `installContainerEdge`.
|
|
1661
1717
|
installs: {
|
|
1662
1718
|
docker: dockerInstallPlan,
|
|
1663
|
-
git:
|
|
1664
|
-
rsync:
|
|
1665
|
-
}
|
|
1666
|
-
};
|
|
1667
|
-
|
|
1668
|
-
// ../../packages/adapters/src/system/elevated-executor.ts
|
|
1669
|
-
var INSTALL_ENV = "DEBIAN_FRONTEND=noninteractive DPKG_FORCE=confnew";
|
|
1670
|
-
function elevateCommand(command) {
|
|
1671
|
-
return `sudo -n sh -c ${sq(`export ${INSTALL_ENV}; ${command}`)}`;
|
|
1672
|
-
}
|
|
1673
|
-
function dirOf(path) {
|
|
1674
|
-
const idx = path.lastIndexOf("/");
|
|
1675
|
-
if (idx < 0) return ".";
|
|
1676
|
-
return idx === 0 ? "/" : path.slice(0, idx);
|
|
1677
|
-
}
|
|
1678
|
-
function elevatedExecutor(inner) {
|
|
1679
|
-
const writeFileElevated = async (path, content) => {
|
|
1680
|
-
const tmp = `/tmp/.openship-elev-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
1681
|
-
await inner.writeFile(tmp, content);
|
|
1682
|
-
await inner.exec(
|
|
1683
|
-
elevateCommand(`mkdir -p ${sq(dirOf(path))} && mv -f ${sq(tmp)} ${sq(path)}`)
|
|
1684
|
-
);
|
|
1685
|
-
};
|
|
1686
|
-
const overrides = {
|
|
1687
|
-
exec: (command, opts) => inner.exec(elevateCommand(command), opts),
|
|
1688
|
-
streamExec: (command, onLog, opts) => inner.streamExec(elevateCommand(command), onLog, opts),
|
|
1689
|
-
writeFile: writeFileElevated,
|
|
1690
|
-
// Elevated for the same reason as writeFile: the vhost/conf dirs are root-owned,
|
|
1691
|
-
// and the Proxy would otherwise forward this to the inner executor unelevated —
|
|
1692
|
-
// EACCES immediately after an elevated write succeeded.
|
|
1693
|
-
rename: async (from, to) => {
|
|
1694
|
-
await inner.exec(elevateCommand(`mv -f ${sq(from)} ${sq(to)}`));
|
|
1695
|
-
},
|
|
1696
|
-
mkdir: async (path) => {
|
|
1697
|
-
await inner.exec(elevateCommand(`mkdir -p ${sq(path)}`));
|
|
1698
|
-
},
|
|
1699
|
-
rm: async (path) => {
|
|
1700
|
-
await inner.exec(elevateCommand(`rm -rf ${sq(path)}`));
|
|
1701
|
-
}
|
|
1702
|
-
};
|
|
1703
|
-
return new Proxy(inner, {
|
|
1704
|
-
get(target, prop2) {
|
|
1705
|
-
if (Object.prototype.hasOwnProperty.call(overrides, prop2)) {
|
|
1706
|
-
return overrides[prop2];
|
|
1707
|
-
}
|
|
1708
|
-
const value = Reflect.get(target, prop2, target);
|
|
1709
|
-
return typeof value === "function" ? value.bind(target) : value;
|
|
1710
|
-
}
|
|
1711
|
-
});
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
// ../../packages/adapters/src/system/installer.ts
|
|
1715
|
-
function log3(message, level = "info") {
|
|
1716
|
-
return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
|
|
1717
|
-
}
|
|
1718
|
-
async function prepareExecutor(executor, component) {
|
|
1719
|
-
const profile = await resolveEnvironment(executor);
|
|
1720
|
-
if (profile.isRoot) return { ok: true, executor, profile };
|
|
1721
|
-
if (profile.canSudo) return { ok: true, executor: elevatedExecutor(executor), profile };
|
|
1722
|
-
return {
|
|
1723
|
-
ok: false,
|
|
1724
|
-
result: {
|
|
1725
|
-
component,
|
|
1726
|
-
success: false,
|
|
1727
|
-
error: `Installing ${component} needs root. Connect this server as root, or as a user with passwordless sudo.`
|
|
1728
|
-
}
|
|
1729
|
-
};
|
|
1730
|
-
}
|
|
1731
|
-
async function installDocker(executor, onLog) {
|
|
1732
|
-
const prep = await prepareExecutor(executor, "docker");
|
|
1733
|
-
if (!prep.ok) return prep.result;
|
|
1734
|
-
executor = prep.executor;
|
|
1735
|
-
const profile = prep.profile;
|
|
1736
|
-
const plan = systemCatalog.installs.docker(profile);
|
|
1737
|
-
if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
|
|
1738
|
-
return { component: "docker", success: false, error: plan.unsupportedReason ?? "Docker install not supported" };
|
|
1739
|
-
}
|
|
1740
|
-
onLog(log3("Installing Docker Engine..."));
|
|
1741
|
-
try {
|
|
1742
|
-
const { code } = await executor.streamExec(plan.installCommand, onLog);
|
|
1743
|
-
if (code !== 0) return { component: "docker", success: false, error: "Docker install failed" };
|
|
1744
|
-
if (plan.startCommand) {
|
|
1745
|
-
onLog(log3("Starting Docker service..."));
|
|
1746
|
-
await executor.streamExec(plan.startCommand, onLog);
|
|
1747
|
-
}
|
|
1748
|
-
const version = await executor.exec(plan.verifyCommand);
|
|
1749
|
-
const parsed = systemCatalog.checks.docker.parseVersion(version);
|
|
1750
|
-
onLog(log3(`Docker ${parsed} installed`));
|
|
1751
|
-
return { component: "docker", success: true, version: parsed };
|
|
1752
|
-
} catch (err) {
|
|
1753
|
-
const msg = safeErrorMessage(err);
|
|
1754
|
-
onLog(log3(`Docker installation failed: ${msg}`, "error"));
|
|
1755
|
-
return { component: "docker", success: false, error: msg };
|
|
1756
|
-
}
|
|
1757
|
-
}
|
|
1758
|
-
async function installGit(executor, onLog, opts) {
|
|
1759
|
-
const prep = await prepareExecutor(executor, "git");
|
|
1760
|
-
if (!prep.ok) return prep.result;
|
|
1761
|
-
executor = prep.executor;
|
|
1762
|
-
const profile = prep.profile;
|
|
1763
|
-
const plan = systemCatalog.installs.git(profile);
|
|
1764
|
-
if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
|
|
1765
|
-
return { component: "git", success: false, error: plan.unsupportedReason ?? "Git install not supported" };
|
|
1766
|
-
}
|
|
1767
|
-
onLog(log3(opts?.label ? `Installing Git in ${opts.label}...` : "Installing Git..."));
|
|
1768
|
-
try {
|
|
1769
|
-
const { code } = await executor.streamExec(plan.installCommand, onLog);
|
|
1770
|
-
if (code !== 0) return { component: "git", success: false, error: "Git installation failed" };
|
|
1771
|
-
const version = await executor.exec(plan.verifyCommand);
|
|
1772
|
-
const parsed = systemCatalog.checks.git.parseVersion(version);
|
|
1773
|
-
onLog(log3(opts?.label ? `Git ${parsed} installed in ${opts.label}` : `Git ${parsed} installed`));
|
|
1774
|
-
return { component: "git", success: true, version: parsed };
|
|
1775
|
-
} catch (err) {
|
|
1776
|
-
const msg = safeErrorMessage(err);
|
|
1777
|
-
onLog(log3(`Git installation failed: ${msg}`, "error"));
|
|
1778
|
-
return { component: "git", success: false, error: msg };
|
|
1779
|
-
}
|
|
1780
|
-
}
|
|
1781
|
-
async function installRsync(executor, onLog) {
|
|
1782
|
-
const prep = await prepareExecutor(executor, "rsync");
|
|
1783
|
-
if (!prep.ok) return prep.result;
|
|
1784
|
-
executor = prep.executor;
|
|
1785
|
-
const profile = prep.profile;
|
|
1786
|
-
const plan = systemCatalog.installs.rsync(profile);
|
|
1787
|
-
if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
|
|
1788
|
-
return { component: "rsync", success: false, error: plan.unsupportedReason ?? "rsync install not supported" };
|
|
1789
|
-
}
|
|
1790
|
-
onLog(log3("Installing rsync..."));
|
|
1791
|
-
try {
|
|
1792
|
-
const { code } = await executor.streamExec(plan.installCommand, onLog);
|
|
1793
|
-
if (code !== 0) return { component: "rsync", success: false, error: "rsync installation failed" };
|
|
1794
|
-
const version = await executor.exec(plan.verifyCommand);
|
|
1795
|
-
const parsed = systemCatalog.checks.rsync.parseVersion(version);
|
|
1796
|
-
onLog(log3(`rsync ${parsed} installed`));
|
|
1797
|
-
return { component: "rsync", success: true, version: parsed };
|
|
1798
|
-
} catch (err) {
|
|
1799
|
-
const msg = safeErrorMessage(err);
|
|
1800
|
-
onLog(log3(`rsync installation failed: ${msg}`, "error"));
|
|
1801
|
-
return { component: "rsync", success: false, error: msg };
|
|
1802
|
-
}
|
|
1803
|
-
}
|
|
1804
|
-
async function installContainerEdge(executor, onLog, config) {
|
|
1805
|
-
const prep = await prepareExecutor(executor, "edge");
|
|
1806
|
-
if (!prep.ok) return prep.result;
|
|
1807
|
-
executor = prep.executor;
|
|
1808
|
-
if (!await dockerAvailable(executor)) {
|
|
1809
|
-
const error = "The edge is a container image and needs Docker on this server. Install the Docker component first, then install the edge.";
|
|
1810
|
-
onLog(log3(error, "error"));
|
|
1811
|
-
return { component: "edge", success: false, error };
|
|
1719
|
+
git: (profile) => packagePlan(profile, "git", "git --version"),
|
|
1720
|
+
rsync: (profile) => packagePlan(profile, "rsync", "rsync --version | head -n 1")
|
|
1812
1721
|
}
|
|
1813
|
-
try {
|
|
1814
|
-
const result = await ensureContainerEdge(executor, {
|
|
1815
|
-
onLog,
|
|
1816
|
-
config,
|
|
1817
|
-
image: config?.edgeImage
|
|
1818
|
-
});
|
|
1819
|
-
if (result.converted) {
|
|
1820
|
-
onLog(log3("Edge moved from host OpenResty to the container edge", "warn"));
|
|
1821
|
-
}
|
|
1822
|
-
const version = await executor.exec(containerCommand(result.container, "openresty -v 2>&1")).catch(() => "");
|
|
1823
|
-
return {
|
|
1824
|
-
component: "edge",
|
|
1825
|
-
success: true,
|
|
1826
|
-
version: version ? systemCatalog.checks.openresty.parseVersion(version) : void 0
|
|
1827
|
-
};
|
|
1828
|
-
} catch (err) {
|
|
1829
|
-
if (err instanceof EdgeMigrateRequested) throw err;
|
|
1830
|
-
const msg = safeErrorMessage(err);
|
|
1831
|
-
onLog(log3(`Edge setup failed: ${msg}`, "error"));
|
|
1832
|
-
return { component: "edge", success: false, error: msg };
|
|
1833
|
-
}
|
|
1834
|
-
}
|
|
1835
|
-
var COMPONENT_INSTALLERS = {
|
|
1836
|
-
docker: (exec, log6) => installDocker(exec, log6),
|
|
1837
|
-
// The edge is ONE component and it is a container image — there is no
|
|
1838
|
-
// host-OpenResty or host-certbot installer to reach for any more.
|
|
1839
|
-
edge: (exec, log6, config) => installContainerEdge(exec, log6, config),
|
|
1840
|
-
git: (exec, log6) => installGit(exec, log6),
|
|
1841
|
-
rsync: (exec, log6) => installRsync(exec, log6)
|
|
1842
1722
|
};
|
|
1843
1723
|
|
|
1844
1724
|
// ../../packages/adapters/src/system/available-version.ts
|
|
@@ -1858,32 +1738,30 @@ function normalizePkgVersion(raw) {
|
|
|
1858
1738
|
if (dash !== -1) v = v.slice(0, dash);
|
|
1859
1739
|
return v.trim();
|
|
1860
1740
|
}
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
const
|
|
1872
|
-
return out?.match(/Candidate:\s*(\S+)/)?.[1]?.trim() ?? null;
|
|
1873
|
-
}
|
|
1874
|
-
if (pm === "dnf" || pm === "yum") {
|
|
1875
|
-
const out = await tryExec3(
|
|
1876
|
-
executor,
|
|
1877
|
-
`${pm} -q --cacheonly list available ${q} 2>/dev/null | tail -n1`
|
|
1878
|
-
);
|
|
1879
|
-
return out?.trim().split(/\s+/)[1]?.trim() ?? null;
|
|
1880
|
-
}
|
|
1881
|
-
if (pm === "apk") {
|
|
1882
|
-
const out = await tryExec3(executor, `apk policy ${q} 2>/dev/null`);
|
|
1883
|
-
const versions = [...out?.matchAll(/^\s+([\w.+~-]+):\s*$/gm) ?? []].map((m) => m[1]);
|
|
1741
|
+
var READ_CANDIDATE = {
|
|
1742
|
+
// `apt-cache policy` prints " Candidate: <version>".
|
|
1743
|
+
apt: (out) => out.match(/Candidate:\s*(\S+)/)?.[1]?.trim() ?? null,
|
|
1744
|
+
// Cached list; the last "pkg.arch version repo" row carries the newest avail.
|
|
1745
|
+
dnf: (out) => out.trim().split(/\s+/)[1]?.trim() ?? null,
|
|
1746
|
+
yum: (out) => out.trim().split(/\s+/)[1]?.trim() ?? null,
|
|
1747
|
+
// `apk policy` prints one ` <version>:` line per available version (colon at end
|
|
1748
|
+
// of line), each followed by indented source lines (installed marker, repo URLs);
|
|
1749
|
+
// the last version line is newest.
|
|
1750
|
+
apk: (out) => {
|
|
1751
|
+
const versions = [...out.matchAll(/^\s+([\w.+~-]+):\s*$/gm)].map((m) => m[1]);
|
|
1884
1752
|
return versions.length ? versions[versions.length - 1] : null;
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1753
|
+
},
|
|
1754
|
+
// Both refuse the probe itself, so there is no output to read. Stated rather than
|
|
1755
|
+
// omitted — `null` here is paired with a refusal, not with a missing branch.
|
|
1756
|
+
brew: null,
|
|
1757
|
+
none: null
|
|
1758
|
+
};
|
|
1759
|
+
async function probeCandidate(executor, profile, pkg) {
|
|
1760
|
+
const read = READ_CANDIDATE[profile.packageManager];
|
|
1761
|
+
const probe = envOps(profile).pkgAvailableVersion(pkg);
|
|
1762
|
+
if (!read || !probe.supported) return null;
|
|
1763
|
+
const out = await tryExec(executor, opScript(probe.value));
|
|
1764
|
+
return out ? read(out) : null;
|
|
1887
1765
|
}
|
|
1888
1766
|
async function enrichAvailableVersions(executor, profile, statuses) {
|
|
1889
1767
|
const targets = statuses.filter(
|
|
@@ -1893,7 +1771,7 @@ async function enrichAvailableVersions(executor, profile, statuses) {
|
|
|
1893
1771
|
targets.map(async (s) => {
|
|
1894
1772
|
try {
|
|
1895
1773
|
const pkg = PACKAGE_NAMES[s.name];
|
|
1896
|
-
const candidate = await probeCandidate(executor, profile
|
|
1774
|
+
const candidate = await probeCandidate(executor, profile, pkg);
|
|
1897
1775
|
if (!candidate) return;
|
|
1898
1776
|
const avail = normalizePkgVersion(candidate);
|
|
1899
1777
|
const installed = normalizePkgVersion(s.version);
|
|
@@ -1957,37 +1835,8 @@ function getSystemComponentDefinition(name) {
|
|
|
1957
1835
|
}
|
|
1958
1836
|
|
|
1959
1837
|
// ../../packages/adapters/src/system/checks.ts
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
systemDebug("checks", `exec:start ${command}`);
|
|
1963
|
-
try {
|
|
1964
|
-
const result = await executor.exec(command, { timeout: 1e4 });
|
|
1965
|
-
systemDebug(
|
|
1966
|
-
"checks",
|
|
1967
|
-
`exec:ok ${command} (${formatDuration(startedAt)})`
|
|
1968
|
-
);
|
|
1969
|
-
return { output: result.trim(), error: null };
|
|
1970
|
-
} catch (err) {
|
|
1971
|
-
if (isRemoteConnectionError(err)) {
|
|
1972
|
-
systemDebug(
|
|
1973
|
-
"checks",
|
|
1974
|
-
`exec:abort ${command} (${formatDuration(startedAt)}) ${safeErrorMessage(err)}`
|
|
1975
|
-
);
|
|
1976
|
-
throw err;
|
|
1977
|
-
}
|
|
1978
|
-
const msg = safeErrorMessage(err);
|
|
1979
|
-
systemDebug(
|
|
1980
|
-
"checks",
|
|
1981
|
-
`exec:fail ${command} (${formatDuration(startedAt)}) ${msg}`
|
|
1982
|
-
);
|
|
1983
|
-
return { output: "", error: msg || `\`${command}\` failed` };
|
|
1984
|
-
}
|
|
1985
|
-
}
|
|
1986
|
-
function withReason(message, error) {
|
|
1987
|
-
if (!error) return message;
|
|
1988
|
-
const first = error.split("\n").map((line) => line.trim()).find(Boolean);
|
|
1989
|
-
if (!first) return message;
|
|
1990
|
-
return `${message} \u2014 ${first.length > 200 ? `${first.slice(0, 199)}\u2026` : first}`;
|
|
1838
|
+
function isSocketDenied(error) {
|
|
1839
|
+
return error != null && /permission denied|eacces/i.test(error);
|
|
1991
1840
|
}
|
|
1992
1841
|
function healthy(name, version, running) {
|
|
1993
1842
|
const component = getSystemComponentDefinition(name);
|
|
@@ -2020,22 +1869,31 @@ function unhealthy(name, message, opts) {
|
|
|
2020
1869
|
async function checkDocker(executor) {
|
|
2021
1870
|
const startedAt = Date.now();
|
|
2022
1871
|
const recipe = systemCatalog.checks.docker;
|
|
2023
|
-
const version = await
|
|
1872
|
+
const version = await probeExec(executor, recipe.versionCommand, "checks");
|
|
2024
1873
|
if (!version.output) {
|
|
2025
1874
|
systemDebug("checks", `docker:missing (${formatDuration(startedAt)})`);
|
|
2026
1875
|
return unhealthy("docker", withReason(recipe.missingMessage, version.error));
|
|
2027
1876
|
}
|
|
2028
1877
|
const parsed = recipe.parseVersion(version.output);
|
|
2029
|
-
const info = await
|
|
1878
|
+
const info = await probeExec(executor, recipe.daemonCommand, "checks");
|
|
2030
1879
|
if (!info.output) {
|
|
1880
|
+
const denied = isSocketDenied(info.error);
|
|
2031
1881
|
systemDebug(
|
|
2032
1882
|
"checks",
|
|
2033
|
-
`docker:not-running (${formatDuration(startedAt)}) ${info.error ?? "probe exited 0 with no server version"}`
|
|
1883
|
+
`docker:${denied ? "denied" : "not-running"} (${formatDuration(startedAt)}) ${info.error ?? "probe exited 0 with no server version"}`
|
|
1884
|
+
);
|
|
1885
|
+
return unhealthy(
|
|
1886
|
+
"docker",
|
|
1887
|
+
withReason(denied && recipe.deniedMessage || recipe.notRunningMessage, info.error),
|
|
1888
|
+
{
|
|
1889
|
+
version: parsed,
|
|
1890
|
+
// Left UNSET when denied: `running: false` would be a fact we don't have. The
|
|
1891
|
+
// permission check happens before the daemon is consulted, so a refusal is
|
|
1892
|
+
// equally consistent with a healthy daemon, and asserting it is stopped is the
|
|
1893
|
+
// same class of guess the message above stopped making.
|
|
1894
|
+
...denied ? {} : { running: false }
|
|
1895
|
+
}
|
|
2034
1896
|
);
|
|
2035
|
-
return unhealthy("docker", withReason(recipe.notRunningMessage, info.error), {
|
|
2036
|
-
version: parsed,
|
|
2037
|
-
running: false
|
|
2038
|
-
});
|
|
2039
1897
|
}
|
|
2040
1898
|
systemDebug("checks", `docker:healthy (${formatDuration(startedAt)})`);
|
|
2041
1899
|
return healthy("docker", parsed, true);
|
|
@@ -2043,7 +1901,7 @@ async function checkDocker(executor) {
|
|
|
2043
1901
|
async function checkGit(executor) {
|
|
2044
1902
|
const startedAt = Date.now();
|
|
2045
1903
|
const recipe = systemCatalog.checks.git;
|
|
2046
|
-
const version = await
|
|
1904
|
+
const version = await probeExec(executor, recipe.versionCommand, "checks");
|
|
2047
1905
|
if (!version.output) {
|
|
2048
1906
|
systemDebug("checks", `git:missing (${formatDuration(startedAt)})`);
|
|
2049
1907
|
return unhealthy("git", withReason(recipe.missingMessage, version.error));
|
|
@@ -2055,7 +1913,7 @@ async function checkGit(executor) {
|
|
|
2055
1913
|
async function checkRsync(executor) {
|
|
2056
1914
|
const startedAt = Date.now();
|
|
2057
1915
|
const recipe = systemCatalog.checks.rsync;
|
|
2058
|
-
const version = await
|
|
1916
|
+
const version = await probeExec(executor, recipe.versionCommand, "checks");
|
|
2059
1917
|
if (!version.output) {
|
|
2060
1918
|
systemDebug("checks", `rsync:missing (${formatDuration(startedAt)})`);
|
|
2061
1919
|
return unhealthy("rsync", withReason(recipe.missingMessage, version.error));
|
|
@@ -2083,7 +1941,7 @@ async function checkEdge(executor) {
|
|
|
2083
1941
|
{ running: false }
|
|
2084
1942
|
);
|
|
2085
1943
|
}
|
|
2086
|
-
const version = await
|
|
1944
|
+
const version = await probeExec(executor, containerCommand(container, "openresty -v 2>&1"), "checks");
|
|
2087
1945
|
if (!version.output) {
|
|
2088
1946
|
systemDebug("checks", `edge:container-unresponsive (${formatDuration(startedAt)})`);
|
|
2089
1947
|
return unhealthy(
|
|
@@ -2159,6 +2017,182 @@ async function checkComponents(executor, names) {
|
|
|
2159
2017
|
return results;
|
|
2160
2018
|
}
|
|
2161
2019
|
|
|
2020
|
+
// ../../packages/adapters/src/system/installer.ts
|
|
2021
|
+
function log3(message, level = "info") {
|
|
2022
|
+
return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
|
|
2023
|
+
}
|
|
2024
|
+
async function prepareExecutor(executor, component) {
|
|
2025
|
+
const grant = await privilegedExecutor(executor, `Installing ${component}`);
|
|
2026
|
+
if (!grant.supported) {
|
|
2027
|
+
return { ok: false, result: { component, success: false, error: grant.reason } };
|
|
2028
|
+
}
|
|
2029
|
+
return { ok: true, executor: grant.value.executor, profile: grant.value.profile };
|
|
2030
|
+
}
|
|
2031
|
+
function describeInstallFailure(label, code, output) {
|
|
2032
|
+
const lines = output.split("\n").map((line) => line.replace(/\r/g, "").trim()).filter(Boolean);
|
|
2033
|
+
const flagged = lines.filter((line) => /^(e:|err:|error:|dpkg:|fatal:)/i.test(line));
|
|
2034
|
+
const picked = (flagged.length ? flagged : lines).slice(-3).join(" \xB7 ");
|
|
2035
|
+
const detail = picked.length > 400 ? `${picked.slice(0, 399)}\u2026` : picked;
|
|
2036
|
+
const held = /held packages?|--allow-change-held-packages/i.test(output) ? " Packages are held on this host (`apt-mark hold`) and Openship does not override a hold \u2014 unhold them first if you do want this install." : "";
|
|
2037
|
+
return `${label} install failed (exit ${code})${detail ? `: ${detail}` : ""}.${held}`;
|
|
2038
|
+
}
|
|
2039
|
+
async function probeDocker(executor) {
|
|
2040
|
+
const status = await checkDocker(executor);
|
|
2041
|
+
const version = status.version;
|
|
2042
|
+
if (!status.installed) return { verdict: "missing", message: status.message };
|
|
2043
|
+
if (!status.healthy) return { verdict: "stopped", version, message: status.message };
|
|
2044
|
+
if (version && compareSemver(version, MIN_DOCKER_VERSION) < 0) {
|
|
2045
|
+
return { verdict: "outdated", version, message: status.message };
|
|
2046
|
+
}
|
|
2047
|
+
return { verdict: "ok", version, message: status.message };
|
|
2048
|
+
}
|
|
2049
|
+
function dockerAlreadyThere(present, onLog) {
|
|
2050
|
+
const named = present.version ? `Docker ${present.version}` : "Docker";
|
|
2051
|
+
onLog(log3(`${named} is installed and its daemon is answering \u2014 nothing to install`));
|
|
2052
|
+
return { component: "docker", success: true, version: present.version };
|
|
2053
|
+
}
|
|
2054
|
+
async function installDocker(executor, onLog, config) {
|
|
2055
|
+
const forced = config?.reinstall === true;
|
|
2056
|
+
let present = forced ? null : await probeDocker(executor);
|
|
2057
|
+
if (present?.verdict === "ok") return dockerAlreadyThere(present, onLog);
|
|
2058
|
+
const prep = await prepareExecutor(executor, "docker");
|
|
2059
|
+
if (!prep.ok) return prep.result;
|
|
2060
|
+
executor = prep.executor;
|
|
2061
|
+
const profile = prep.profile;
|
|
2062
|
+
if (present && present.verdict !== "missing" && !profile.isRoot) {
|
|
2063
|
+
present = await probeDocker(executor);
|
|
2064
|
+
if (present.verdict === "ok") return dockerAlreadyThere(present, onLog);
|
|
2065
|
+
}
|
|
2066
|
+
const plan = systemCatalog.installs.docker(profile);
|
|
2067
|
+
const service = plan.supported ? plan.value.service : plan;
|
|
2068
|
+
if (present?.verdict === "stopped" && service?.supported) {
|
|
2069
|
+
onLog(log3(`${present.message} \u2014 starting the Docker service`, "warn"));
|
|
2070
|
+
await executor.streamExec(service.value, onLog).catch(() => void 0);
|
|
2071
|
+
present = await probeDocker(executor);
|
|
2072
|
+
if (present.verdict === "ok") {
|
|
2073
|
+
onLog(log3(`Docker ${present.version} is running`));
|
|
2074
|
+
return { component: "docker", success: true, version: present.version };
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
if (present && present.verdict !== "missing") {
|
|
2078
|
+
const cannotStart = present.verdict === "stopped" && service && !service.supported ? ` Openship could not start it for you: ${service.reason}` : "";
|
|
2079
|
+
const error = (present.verdict === "outdated" ? `Docker ${present.version} is installed but Openship needs ${MIN_DOCKER_VERSION} or newer. Upgrading Docker restarts the daemon and every container on this server, so it is never done as part of another step \u2014 upgrade it on the host, or use Reinstall on the Docker component to run the official installer.` : `${present.message}. Start Docker on this server and retry. Openship will not reinstall Docker over a daemon that is merely unreachable: the installer would also upgrade the engine and restart every container on this host. Use Reinstall on the Docker component if that is what you want.`) + cannotStart;
|
|
2080
|
+
onLog(log3(error, "error"));
|
|
2081
|
+
return { component: "docker", success: false, version: present.version, error };
|
|
2082
|
+
}
|
|
2083
|
+
if (!plan.supported) {
|
|
2084
|
+
onLog(log3(plan.reason, "error"));
|
|
2085
|
+
return { component: "docker", success: false, error: plan.reason };
|
|
2086
|
+
}
|
|
2087
|
+
onLog(
|
|
2088
|
+
forced ? log3(
|
|
2089
|
+
"Running the Docker installer \u2014 it may upgrade the engine, which restarts the daemon and every container on this host...",
|
|
2090
|
+
"warn"
|
|
2091
|
+
) : log3("Installing Docker Engine...")
|
|
2092
|
+
);
|
|
2093
|
+
try {
|
|
2094
|
+
const { code, output } = await executor.streamExec(
|
|
2095
|
+
plan.value.installCommand,
|
|
2096
|
+
onLog
|
|
2097
|
+
);
|
|
2098
|
+
if (code !== 0) {
|
|
2099
|
+
const error = describeInstallFailure("Docker", code, output);
|
|
2100
|
+
onLog(log3(error, "error"));
|
|
2101
|
+
return { component: "docker", success: false, error };
|
|
2102
|
+
}
|
|
2103
|
+
if (service?.supported) {
|
|
2104
|
+
onLog(log3("Starting Docker service..."));
|
|
2105
|
+
await executor.streamExec(service.value, onLog);
|
|
2106
|
+
} else if (service) {
|
|
2107
|
+
onLog(log3(`Docker is installed but Openship cannot start it here: ${service.reason}`, "warn"));
|
|
2108
|
+
}
|
|
2109
|
+
const version = await executor.exec(plan.value.verifyCommand);
|
|
2110
|
+
const parsed = systemCatalog.checks.docker.parseVersion(version);
|
|
2111
|
+
onLog(log3(`Docker ${parsed} installed`));
|
|
2112
|
+
return { component: "docker", success: true, version: parsed };
|
|
2113
|
+
} catch (err) {
|
|
2114
|
+
const msg = safeErrorMessage(err);
|
|
2115
|
+
onLog(log3(`Docker installation failed: ${msg}`, "error"));
|
|
2116
|
+
return { component: "docker", success: false, error: msg };
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
async function installPackaged(executor, onLog, component, label, where) {
|
|
2120
|
+
const prep = await prepareExecutor(executor, component);
|
|
2121
|
+
if (!prep.ok) return prep.result;
|
|
2122
|
+
executor = prep.executor;
|
|
2123
|
+
const plan = systemCatalog.installs[component](prep.profile);
|
|
2124
|
+
if (!plan.supported) {
|
|
2125
|
+
onLog(log3(plan.reason, "error"));
|
|
2126
|
+
return { component, success: false, error: plan.reason };
|
|
2127
|
+
}
|
|
2128
|
+
const inside = where ? ` in ${where}` : "";
|
|
2129
|
+
onLog(log3(`Installing ${label}${inside}...`));
|
|
2130
|
+
try {
|
|
2131
|
+
const { code, output } = await executor.streamExec(
|
|
2132
|
+
plan.value.installCommand,
|
|
2133
|
+
onLog
|
|
2134
|
+
);
|
|
2135
|
+
if (code !== 0) {
|
|
2136
|
+
const error = describeInstallFailure(label, code, output);
|
|
2137
|
+
onLog(log3(error, "error"));
|
|
2138
|
+
return { component, success: false, error };
|
|
2139
|
+
}
|
|
2140
|
+
const version = await executor.exec(plan.value.verifyCommand);
|
|
2141
|
+
const parsed = systemCatalog.checks[component].parseVersion(version);
|
|
2142
|
+
onLog(log3(`${label} ${parsed} installed${inside}`));
|
|
2143
|
+
return { component, success: true, version: parsed };
|
|
2144
|
+
} catch (err) {
|
|
2145
|
+
const msg = safeErrorMessage(err);
|
|
2146
|
+
onLog(log3(`${label} installation failed: ${msg}`, "error"));
|
|
2147
|
+
return { component, success: false, error: msg };
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
function installGit(executor, onLog, opts) {
|
|
2151
|
+
return installPackaged(executor, onLog, "git", "Git", opts?.label);
|
|
2152
|
+
}
|
|
2153
|
+
function installRsync(executor, onLog) {
|
|
2154
|
+
return installPackaged(executor, onLog, "rsync", "rsync");
|
|
2155
|
+
}
|
|
2156
|
+
async function installContainerEdge(executor, onLog, config) {
|
|
2157
|
+
const prep = await prepareExecutor(executor, "edge");
|
|
2158
|
+
if (!prep.ok) return prep.result;
|
|
2159
|
+
executor = prep.executor;
|
|
2160
|
+
if (!await dockerAvailable(executor)) {
|
|
2161
|
+
const error = "The edge is a container image and needs Docker on this server. Install the Docker component first, then install the edge.";
|
|
2162
|
+
onLog(log3(error, "error"));
|
|
2163
|
+
return { component: "edge", success: false, error };
|
|
2164
|
+
}
|
|
2165
|
+
try {
|
|
2166
|
+
const result = await ensureContainerEdge(executor, {
|
|
2167
|
+
onLog,
|
|
2168
|
+
config,
|
|
2169
|
+
image: config?.edgeImage
|
|
2170
|
+
});
|
|
2171
|
+
if (result.converted) {
|
|
2172
|
+
onLog(log3("Edge moved from host OpenResty to the container edge", "warn"));
|
|
2173
|
+
}
|
|
2174
|
+
const version = await executor.exec(containerCommand(result.container, "openresty -v 2>&1")).catch(() => "");
|
|
2175
|
+
return {
|
|
2176
|
+
component: "edge",
|
|
2177
|
+
success: true,
|
|
2178
|
+
version: version ? systemCatalog.checks.openresty.parseVersion(version) : void 0
|
|
2179
|
+
};
|
|
2180
|
+
} catch (err) {
|
|
2181
|
+
if (err instanceof EdgeMigrateRequested) throw err;
|
|
2182
|
+
const msg = safeErrorMessage(err);
|
|
2183
|
+
onLog(log3(`Edge setup failed: ${msg}`, "error"));
|
|
2184
|
+
return { component: "edge", success: false, error: msg };
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
var COMPONENT_INSTALLERS = {
|
|
2188
|
+
docker: (exec, log6, config) => installDocker(exec, log6, config),
|
|
2189
|
+
// The edge is ONE component and it is a container image — there is no
|
|
2190
|
+
// host-OpenResty or host-certbot installer to reach for any more.
|
|
2191
|
+
edge: (exec, log6, config) => installContainerEdge(exec, log6, config),
|
|
2192
|
+
git: (exec, log6) => installGit(exec, log6),
|
|
2193
|
+
rsync: (exec, log6) => installRsync(exec, log6)
|
|
2194
|
+
};
|
|
2195
|
+
|
|
2162
2196
|
// ../../packages/adapters/src/system/proxy/index.ts
|
|
2163
2197
|
async function importSites(executor, status) {
|
|
2164
2198
|
const proxy = status.occupants.find((o) => o.proxy)?.proxy;
|
|
@@ -2206,8 +2240,12 @@ async function ensureEdgeClear(executor, config, onLog) {
|
|
|
2206
2240
|
if (!freed.freed) {
|
|
2207
2241
|
await rollbackEdgeTakeover(executor, onLog);
|
|
2208
2242
|
const plural = freed.stillBound.length > 1;
|
|
2243
|
+
const ports = `port${plural ? "s" : ""} ${freed.stillBound.join(" and ")} ${plural ? "are" : "is"} still in use, so the edge can't bind. Nothing was installed`;
|
|
2209
2244
|
throw new AppError(
|
|
2210
|
-
|
|
2245
|
+
// The advice has to match the actual cause. Told to hunt for another holder, an
|
|
2246
|
+
// operator retries the same unprivileged takeover forever — and "stopped the
|
|
2247
|
+
// existing proxy" is untrue on this arm, since the stop is what was refused.
|
|
2248
|
+
freed.privilegeDegraded ? `The ${ports} \u2014 and Openship could not run the stop as root on this host (see the privilege warning above), so stopping the existing proxy was almost certainly refused. Retrying as this user will fail the same way: reconnect as root, or as a user with passwordless sudo.` : `Stopped the existing proxy, but ${ports} and the previous proxy has been restored \u2014 find what else is holding the port and retry.`,
|
|
2211
2249
|
409,
|
|
2212
2250
|
"EDGE_PORTS_STILL_BOUND"
|
|
2213
2251
|
);
|
|
@@ -2262,15 +2300,22 @@ function setDefaultEdgeImage(image) {
|
|
|
2262
2300
|
function resolveEdgeImage(explicit) {
|
|
2263
2301
|
return buildEdgeImageRef({ explicit, injectedDefault: injectedDefaultImage });
|
|
2264
2302
|
}
|
|
2303
|
+
function edgeHostExecutor(executor) {
|
|
2304
|
+
return rootOrDegrade(executor, {
|
|
2305
|
+
purpose: "Configuring routing and TLS",
|
|
2306
|
+
consequence: "Vhosts and certificates may not be writable, and routing will be incomplete \u2014 the deploy continues and the app still runs on its port.",
|
|
2307
|
+
report: (message) => console.error(`[edge] ${message}`)
|
|
2308
|
+
});
|
|
2309
|
+
}
|
|
2265
2310
|
async function containerEdgeProvider(executor, container, opts) {
|
|
2266
|
-
const { NginxProvider: Provider } = await import("./nginx-
|
|
2311
|
+
const { NginxProvider: Provider } = await import("./nginx-KUFMLMAC.js");
|
|
2267
2312
|
return new Provider({
|
|
2268
2313
|
...opts,
|
|
2269
2314
|
// Vhosts go to the BIND-MOUNTED HOST dir (so every host-side reader — the
|
|
2270
2315
|
// migrate proxy scan, cert reuse, the mail cert symlinks — keeps working),
|
|
2271
2316
|
// while reload/certbot run inside the container.
|
|
2272
2317
|
paths: EDGE_HOST_PATHS,
|
|
2273
|
-
executor: edgeContainerExecutor(executor, container),
|
|
2318
|
+
executor: edgeContainerExecutor(await edgeHostExecutor(executor), container),
|
|
2274
2319
|
// Challenge tokens go to the HOST side of the ACME mount, for the same reason
|
|
2275
2320
|
// vhosts do. This is the ONE mount whose two sides differ
|
|
2276
2321
|
// (/var/lib/openship/edge/acme → /var/www/acme), so the write path and the
|
|
@@ -2287,12 +2332,21 @@ async function containerEdgeProvider(executor, container, opts) {
|
|
|
2287
2332
|
});
|
|
2288
2333
|
}
|
|
2289
2334
|
async function localContainerEdgeProvider(container, opts) {
|
|
2290
|
-
const { DockerEdgeExecutor } = await import("./docker-edge-executor-
|
|
2291
|
-
const { NginxProvider: Provider } = await import("./nginx-
|
|
2335
|
+
const { DockerEdgeExecutor } = await import("./docker-edge-executor-22IBXNYJ.js");
|
|
2336
|
+
const { NginxProvider: Provider } = await import("./nginx-KUFMLMAC.js");
|
|
2292
2337
|
return new Provider({
|
|
2293
2338
|
...opts,
|
|
2294
2339
|
paths: OPENRESTY_DEFAULT_PATHS,
|
|
2295
|
-
|
|
2340
|
+
// The one place the privilege gate is the WRONG instrument, so the reason is stated
|
|
2341
|
+
// instead: this executor's commands run inside the EDGE container, so probing
|
|
2342
|
+
// through it would measure that container's interior and brand the result with a
|
|
2343
|
+
// verdict about the wrong machine. Privilege here comes from the deployment shape —
|
|
2344
|
+
// the api process is root in its own container, the vhost dir is a bind mount it
|
|
2345
|
+
// owns, and the daemon socket is mounted in. There is no login to elevate.
|
|
2346
|
+
executor: rootChecked(
|
|
2347
|
+
new DockerEdgeExecutor({ containerName: container }),
|
|
2348
|
+
"compose: api is root in its own container, writing shared mounts over a mounted socket"
|
|
2349
|
+
),
|
|
2296
2350
|
// nginx.conf + the Lua are BAKED into the image — nothing to detect, install
|
|
2297
2351
|
// or patch, and detection would answer from inside the container anyway.
|
|
2298
2352
|
pinPaths: true,
|
|
@@ -2411,8 +2465,19 @@ async function ensureContainerEdge(executor, opts) {
|
|
|
2411
2465
|
);
|
|
2412
2466
|
}
|
|
2413
2467
|
}
|
|
2468
|
+
const hostState = await rootOrDegrade(executor, {
|
|
2469
|
+
purpose: "Creating the edge's state directories",
|
|
2470
|
+
consequence: "Docker will create them empty, so vhosts and certificates may not persist.",
|
|
2471
|
+
report: (message) => onLog(log5(message, "warn"))
|
|
2472
|
+
});
|
|
2414
2473
|
for (const mount of EDGE_CONTAINER_MOUNTS) {
|
|
2415
|
-
await
|
|
2474
|
+
await hostState.exec(`mkdir -p ${sq(mount.host)}`).catch((err) => {
|
|
2475
|
+
onLog(
|
|
2476
|
+
log5(
|
|
2477
|
+
`Could not create the edge state directory ${mount.host}: ${safeErrorMessage(err)}. Docker will create it empty, so vhosts and certificates may not persist.`,
|
|
2478
|
+
"warn"
|
|
2479
|
+
)
|
|
2480
|
+
);
|
|
2416
2481
|
});
|
|
2417
2482
|
}
|
|
2418
2483
|
const clear = await ensureEdgeClear(executor, opts.config, onLog);
|
|
@@ -2472,6 +2537,7 @@ export {
|
|
|
2472
2537
|
verifyEdgeServing,
|
|
2473
2538
|
ensureContainerEdge,
|
|
2474
2539
|
checkGit,
|
|
2540
|
+
COMPONENT_CHECKS,
|
|
2475
2541
|
checkAll,
|
|
2476
2542
|
checkComponents
|
|
2477
2543
|
};
|