openship 0.6.0 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +7 -2
  2. package/dist/{bare-FSUAM5V5.js → bare-XQIPDO4R.js} +10 -9
  3. package/dist/{chunk-PSZSKEPA.js → chunk-3ZAGC5NT.js} +64 -17
  4. package/dist/{chunk-6ODUBKUB.js → chunk-5OUWIPOB.js} +69 -7
  5. package/dist/chunk-6HOA5JT7.js +124 -0
  6. package/dist/{chunk-TWJCLD4W.js → chunk-6JELW75A.js} +4 -4
  7. package/dist/{chunk-GUQ3QCJ7.js → chunk-7334NSJL.js} +399 -369
  8. package/dist/chunk-7S4USQYF.js +1753 -0
  9. package/dist/{chunk-3LDWWY7U.js → chunk-A7EWM2KA.js} +41 -7
  10. package/dist/chunk-ELGRMZOW.js +25 -0
  11. package/dist/chunk-EZPPTWJQ.js +292 -0
  12. package/dist/{chunk-IXM5245F.js → chunk-GRTXMUHA.js} +553 -538
  13. package/dist/chunk-HPXP6LXZ.js +428 -0
  14. package/dist/{chunk-OTLSJYAC.js → chunk-LEYSL46B.js} +2135 -85
  15. package/dist/{chunk-EMXI4UCU.js → chunk-LFCSC4IU.js} +29 -10
  16. package/dist/{chunk-JIMUYEKN.js → chunk-MSZUCNPV.js} +1 -1
  17. package/dist/{chunk-OGY63DGD.js → chunk-PZRRL5PC.js} +71 -28
  18. package/dist/{chunk-VE2YOK4I.js → chunk-QLX7NBEA.js} +1218 -588
  19. package/dist/chunk-RFWJE7ZN.js +68 -0
  20. package/dist/{chunk-VDDMADOF.js → chunk-TM2IQPYU.js} +16 -2
  21. package/dist/{chunk-3FYIXVX5.js → chunk-VDESJSG6.js} +451 -106
  22. package/dist/{chunk-B3CE2OMR.js → chunk-VHMOQL5Y.js} +276 -50
  23. package/dist/{chunk-QEH4QUBX.js → chunk-VLQ3UIVO.js} +393 -53
  24. package/dist/{chunk-VZ2ZHPS2.js → chunk-WE7BXW6L.js} +3 -9
  25. package/dist/chunk-WFRXYWK2.js +444 -0
  26. package/dist/chunk-XRNRT37W.js +316 -0
  27. package/dist/{chunk-EP2TNALB.js → chunk-XVOBL7BC.js} +31 -8
  28. package/dist/cloud-3DODOWTR.js +36 -0
  29. package/dist/{detect-ALF4GXRX.js → detect-P4CIQBRH.js} +3 -3
  30. package/dist/{docker-QECQJ5P4.js → docker-3K2NEBD6.js} +15 -8
  31. package/dist/{docker-edge-executor-62N64MGS.js → docker-edge-executor-YZZ6P7IC.js} +5 -5
  32. package/dist/{edge-import-CQWHN2Q3.js → edge-import-MCJNVTZ2.js} +26 -23
  33. package/dist/edge-preflight-ES6FH4JK.js +42 -0
  34. package/dist/{ensure-container-edge-MJVXJQG5.js → ensure-container-edge-YPITNH45.js} +9 -8
  35. package/dist/{executor-JELARVKC.js → executor-DZGX55K2.js} +12 -5
  36. package/dist/host-channel-preflight-MUXE3KZU.js +49 -0
  37. package/dist/index.js +3668 -4063
  38. package/dist/{nginx-AXGNWRKZ.js → nginx-NSAHWGJV.js} +11 -5
  39. package/dist/node-bootstrap.js +241 -0
  40. package/dist/node-entry.js +52 -1
  41. package/dist/{openresty-lua-IE4P5YQX.js → openresty-lua-DR4D3EZ6.js} +6 -2
  42. package/dist/privilege-YTVI4SKF.js +19 -0
  43. package/dist/server/engine/samples/fail2ban/jail.d/sshd.local +4 -1
  44. package/dist/server/index.js +21501 -13137
  45. package/dist/server/lua/geo_country.lua +4 -3
  46. package/dist/server/lua/pipe_stream.lua +52 -39
  47. package/dist/server/lua/site_logger.lua +22 -24
  48. package/dist/server/migrations/0098_instance_product_mode.sql +12 -0
  49. package/dist/server/migrations/0099_mail_webmail_project.sql +24 -0
  50. package/dist/server/migrations/0100_server_ssh_private_key.sql +11 -0
  51. package/dist/server/migrations/0101_mail_resume_step.sql +12 -0
  52. package/dist/server/migrations/0102_project_workload_axes.sql +19 -0
  53. package/dist/server/migrations/0103_dns_credential.sql +41 -0
  54. package/dist/server/migrations/0104_instance_host_control.sql +15 -0
  55. package/dist/server/migrations/meta/_journal.json +49 -0
  56. package/dist/{setup-FA4PW4NU.js → setup-WUUZURHS.js} +10 -9
  57. package/dist/up-XEO3SR6Z.js +58 -0
  58. package/package.json +4 -2
  59. package/dist/chunk-IW2UKBVF.js +0 -144
  60. package/dist/chunk-ZCFVHA25.js +0 -89
  61. package/dist/cloud-E5M6VMCG.js +0 -31
  62. package/dist/server/lua/pipe_log.lua +0 -77
  63. /package/dist/{dist-es-JGBPABJI.js → dist-es-ZP5QDSJ3.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,12 @@ import {
17
9
  readDeclaredPair,
18
10
  readMaybeInContainer,
19
11
  validateCertFor
20
- } from "./chunk-QEH4QUBX.js";
12
+ } from "./chunk-VLQ3UIVO.js";
13
+ import {
14
+ privilegedExecutor,
15
+ rootChecked,
16
+ rootOrDegrade
17
+ } from "./chunk-RFWJE7ZN.js";
21
18
  import {
22
19
  EDGE_CONTAINER_NAME,
23
20
  EdgeConflictError,
@@ -32,28 +29,41 @@ import {
32
29
  sanitizeEdgeVhosts,
33
30
  sq as sq2,
34
31
  stopTargetsForStatus
35
- } from "./chunk-VZ2ZHPS2.js";
32
+ } from "./chunk-WE7BXW6L.js";
36
33
  import {
37
- waitForPortListening
38
- } from "./chunk-3LDWWY7U.js";
34
+ probeExec,
35
+ tryExec,
36
+ waitForPortListening,
37
+ withReason
38
+ } from "./chunk-A7EWM2KA.js";
39
39
  import {
40
40
  EDGE_CHALLENGE_HOST_DIR,
41
41
  EDGE_CONTAINER_MOUNTS,
42
42
  EDGE_HOST_PATHS,
43
43
  OPENRESTY_DEFAULT_PATHS
44
- } from "./chunk-OGY63DGD.js";
44
+ } from "./chunk-PZRRL5PC.js";
45
+ import {
46
+ envOps,
47
+ opScript
48
+ } from "./chunk-EZPPTWJQ.js";
49
+ import {
50
+ resolveEnvironment
51
+ } from "./chunk-WFRXYWK2.js";
45
52
  import {
46
53
  sq
47
54
  } from "./chunk-ZFJXPCQP.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
- } from "./chunk-OTLSJYAC.js";
64
+ sanitizeProxySettings,
65
+ systemDebug
66
+ } from "./chunk-LEYSL46B.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 tryExec(executor, cmd) {
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
- containers.set(o.containerName, { name: o.containerName, restart: r?.trim() || "no" });
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.mkdir(JOURNAL_DIR);
158
- await executor.writeFile(JOURNAL_PATH, JSON.stringify(journal, null, 2));
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 raw = await tryExec(executor, `cat ${JOURNAL_PATH} 2>/dev/null`);
167
- if (!raw?.trim()) return null;
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 tryExec(executor, `docker update --restart=no ${sq2(EDGE_CONTAINER_NAME)} 2>/dev/null || true`);
178
- await tryExec(executor, `docker stop ${sq2(EDGE_CONTAINER_NAME)} 2>/dev/null || true`);
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
- executor,
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
- executor,
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
- await tryExec(executor, `docker update --restart=${sq2(c.restart)} ${sq2(c.name)} 2>/dev/null || true`);
191
- await tryExec(executor, `docker start ${sq2(c.name)} 2>/dev/null || true`);
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
- executor,
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
- return freeEdgeTargets(
225
- executor,
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,7 +391,7 @@ 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 tryExec2(executor, `${bin} -T 2>/dev/null`);
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;
@@ -338,7 +399,7 @@ async function dumpResolvedConfig(executor, bins) {
338
399
  async function loadNginxConfig(executor) {
339
400
  const dumped = await dumpResolvedConfig(executor, ["nginx", "openresty"]);
340
401
  if (dumped) return dumped;
341
- const cat = await tryExec2(
402
+ const cat = await tryExec(
342
403
  executor,
343
404
  "cat /etc/nginx/nginx.conf /etc/nginx/sites-enabled/* /etc/nginx/conf.d/*.conf 2>/dev/null"
344
405
  );
@@ -533,11 +594,11 @@ async function scanForeignOpenResty(executor) {
533
594
  }
534
595
  async function scanOpenshipEdge(executor) {
535
596
  const cat = `cat ${OUR_EDGE_SITE_GLOBS.join(" ")} 2>/dev/null`;
536
- const raw = await tryExec2(executor, cat);
597
+ const raw = await tryExec(executor, cat);
537
598
  if (raw?.trim()) return parseNginxConfig(raw);
538
599
  const container = await resolveOurEdgeContainer(executor);
539
600
  if (!container) return parseNginxConfig("");
540
- const fromContainer = await tryExec2(executor, containerCommand(container, cat));
601
+ const fromContainer = await tryExec(executor, containerCommand(container, cat));
541
602
  return parseNginxConfig(fromContainer ?? "");
542
603
  }
543
604
 
@@ -545,12 +606,12 @@ async function scanOpenshipEdge(executor) {
545
606
  var CADDYFILE_PATHS = ["/etc/caddy/Caddyfile", "/etc/Caddyfile"];
546
607
  async function loadCaddyAdminConfig(executor) {
547
608
  const endpoint = "http://localhost:2019/config/";
548
- const out = await tryExec2(executor, `curl -fsS ${endpoint} 2>/dev/null`) || await tryExec2(executor, `wget -qO- ${endpoint} 2>/dev/null`);
609
+ const out = await tryExec(executor, `curl -fsS ${endpoint} 2>/dev/null`) || await tryExec(executor, `wget -qO- ${endpoint} 2>/dev/null`);
549
610
  return out && out.trim().startsWith("{") ? out : null;
550
611
  }
551
612
  async function loadCaddyJson(executor) {
552
613
  for (const p of CADDYFILE_PATHS) {
553
- const out = await tryExec2(executor, `caddy adapt --adapter caddyfile --config ${p} 2>/dev/null`);
614
+ const out = await tryExec(executor, `caddy adapt --adapter caddyfile --config ${p} 2>/dev/null`);
554
615
  if (out && out.trim().startsWith("{")) return out;
555
616
  }
556
617
  return null;
@@ -665,7 +726,7 @@ function parseCaddyJson(raw) {
665
726
  }
666
727
  async function loadCaddyfile(executor) {
667
728
  for (const p of CADDYFILE_PATHS) {
668
- const out = await tryExec2(executor, `cat ${p} 2>/dev/null`);
729
+ const out = await tryExec(executor, `cat ${p} 2>/dev/null`);
669
730
  if (out && out.trim()) return out;
670
731
  }
671
732
  return "";
@@ -800,7 +861,7 @@ async function scanCaddyText(executor) {
800
861
 
801
862
  // ../../packages/adapters/src/system/proxy/import/apache.ts
802
863
  async function vhostFilesFromDump(executor) {
803
- const dump = await tryExec2(
864
+ const dump = await tryExec(
804
865
  executor,
805
866
  "apachectl -S 2>/dev/null || apache2ctl -S 2>/dev/null || httpd -S 2>/dev/null"
806
867
  );
@@ -815,10 +876,10 @@ async function vhostFilesFromDump(executor) {
815
876
  async function loadApacheConfig(executor) {
816
877
  const files = await vhostFilesFromDump(executor);
817
878
  if (files.length > 0) {
818
- const cat2 = await tryExec2(executor, `cat ${files.map(sq3).join(" ")} 2>/dev/null`);
879
+ const cat2 = await tryExec(executor, `cat ${files.map(sq3).join(" ")} 2>/dev/null`);
819
880
  if (cat2 && cat2.trim()) return cat2;
820
881
  }
821
- const cat = await tryExec2(
882
+ const cat = await tryExec(
822
883
  executor,
823
884
  "cat /etc/apache2/sites-enabled/*.conf /etc/apache2/apache2.conf /etc/httpd/conf.d/*.conf /etc/httpd/conf/httpd.conf 2>/dev/null"
824
885
  );
@@ -1145,7 +1206,7 @@ function parseTraefikLabels(containers) {
1145
1206
  };
1146
1207
  }
1147
1208
  async function scanTraefik(executor) {
1148
- const out = await tryExec2(
1209
+ const out = await tryExec(
1149
1210
  executor,
1150
1211
  `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
1212
  );
@@ -1277,10 +1338,10 @@ function candidateNames(host) {
1277
1338
  async function findPairIn(exec, root, host, container) {
1278
1339
  for (const name of candidateNames(host)) {
1279
1340
  const glob = `${root}/certificates/*/${name}/${name}.crt`;
1280
- let certPath = firstCrt(await tryExec2(exec, `ls -1 ${glob} 2>/dev/null`));
1341
+ let certPath = firstCrt(await tryExec(exec, `ls -1 ${glob} 2>/dev/null`));
1281
1342
  if (!certPath && container) {
1282
1343
  certPath = firstCrt(
1283
- await tryExec2(exec, containerCommand(container, `ls -1 ${glob} 2>/dev/null`))
1344
+ await tryExec(exec, containerCommand(container, `ls -1 ${glob} 2>/dev/null`))
1284
1345
  );
1285
1346
  }
1286
1347
  if (certPath) return { certPath, keyPath: certPath.replace(/\.crt$/, ".key") };
@@ -1326,7 +1387,7 @@ async function candidateStoragePaths(exec, container) {
1326
1387
  };
1327
1388
  if (container) {
1328
1389
  collect(
1329
- await tryExec2(
1390
+ await tryExec(
1330
1391
  exec,
1331
1392
  `docker inspect ${sq2(container)} --format '{{json .Config.Cmd}} {{json .Args}} {{json .Config.Env}}' 2>/dev/null`
1332
1393
  )
@@ -1558,61 +1619,21 @@ function makeApi(exec, kind, ours, container) {
1558
1619
  }
1559
1620
 
1560
1621
  // ../../packages/adapters/src/system/catalog.ts
1622
+ var MIN_DOCKER_VERSION = "20.10";
1561
1623
  function dockerInstallPlan(profile) {
1562
- if (profile.os !== "linux") {
1563
- return {
1564
- supported: false,
1565
- unsupportedReason: "Docker installation is only supported on Linux servers"
1566
- };
1567
- }
1568
- return {
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,
1624
+ const ops = envOps(profile);
1625
+ const install = ops.dockerInstall();
1626
+ if (!install.supported) return install;
1627
+ const start = ops.dockerStart();
1628
+ return answered({
1629
+ installCommand: opScript(install.value),
1630
+ service: start.supported ? answered(opScript(start.value)) : start,
1572
1631
  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
- };
1632
+ });
1595
1633
  }
1596
- function rsyncInstallPlan(profile) {
1597
- const commands = {
1598
- apt: "apt-get update -qq && apt-get install -y -qq rsync",
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
- };
1634
+ function packagePlan(profile, pkg, verifyCommand) {
1635
+ const install = envOps(profile).pkgInstall([pkg]);
1636
+ return install.supported ? answered({ installCommand: opScript(install.value), verifyCommand }) : install;
1616
1637
  }
1617
1638
  var systemCatalog = {
1618
1639
  checks: {
@@ -1626,7 +1647,11 @@ var systemCatalog = {
1626
1647
  daemonCommand: "docker info --format '{{.ServerVersion}}'",
1627
1648
  parseVersion: (output) => output.match(/Docker version ([^\s,]+)/)?.[1] ?? output,
1628
1649
  missingMessage: "Docker is not installed",
1629
- notRunningMessage: "Docker is installed but the daemon is not running"
1650
+ notRunningMessage: "Docker is installed but the daemon is not running",
1651
+ // Deliberately does NOT claim the daemon is up or down: the socket's permission
1652
+ // is checked before the daemon is asked, so this arm proves only that we may not
1653
+ // ask. What it can state is the fix, which is the part "not running" got wrong.
1654
+ 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
1655
  },
1631
1656
  openresty: {
1632
1657
  versionCommand: "openresty -v 2>&1 || /usr/local/openresty/bin/openresty -v 2>&1",
@@ -1660,185 +1685,9 @@ var systemCatalog = {
1660
1685
  // CONTAINER. See installer.ts `installContainerEdge`.
1661
1686
  installs: {
1662
1687
  docker: dockerInstallPlan,
1663
- git: gitInstallPlan,
1664
- rsync: rsyncInstallPlan
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 };
1812
- }
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 };
1688
+ git: (profile) => packagePlan(profile, "git", "git --version"),
1689
+ rsync: (profile) => packagePlan(profile, "rsync", "rsync --version | head -n 1")
1833
1690
  }
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
1691
  };
1843
1692
 
1844
1693
  // ../../packages/adapters/src/system/available-version.ts
@@ -1858,32 +1707,30 @@ function normalizePkgVersion(raw) {
1858
1707
  if (dash !== -1) v = v.slice(0, dash);
1859
1708
  return v.trim();
1860
1709
  }
1861
- async function tryExec3(executor, cmd) {
1862
- try {
1863
- return await executor.exec(cmd);
1864
- } catch {
1865
- return null;
1866
- }
1867
- }
1868
- async function probeCandidate(executor, pm, pkg) {
1869
- const q = `'${pkg.replace(/'/g, "'\\''")}'`;
1870
- if (pm === "apt") {
1871
- const out = await tryExec3(executor, `apt-cache policy ${q} 2>/dev/null`);
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]);
1710
+ var READ_CANDIDATE = {
1711
+ // `apt-cache policy` prints " Candidate: <version>".
1712
+ apt: (out) => out.match(/Candidate:\s*(\S+)/)?.[1]?.trim() ?? null,
1713
+ // Cached list; the last "pkg.arch version repo" row carries the newest avail.
1714
+ dnf: (out) => out.trim().split(/\s+/)[1]?.trim() ?? null,
1715
+ yum: (out) => out.trim().split(/\s+/)[1]?.trim() ?? null,
1716
+ // `apk policy` prints one ` <version>:` line per available version (colon at end
1717
+ // of line), each followed by indented source lines (installed marker, repo URLs);
1718
+ // the last version line is newest.
1719
+ apk: (out) => {
1720
+ const versions = [...out.matchAll(/^\s+([\w.+~-]+):\s*$/gm)].map((m) => m[1]);
1884
1721
  return versions.length ? versions[versions.length - 1] : null;
1885
- }
1886
- return null;
1722
+ },
1723
+ // Both refuse the probe itself, so there is no output to read. Stated rather than
1724
+ // omitted — `null` here is paired with a refusal, not with a missing branch.
1725
+ brew: null,
1726
+ none: null
1727
+ };
1728
+ async function probeCandidate(executor, profile, pkg) {
1729
+ const read = READ_CANDIDATE[profile.packageManager];
1730
+ const probe = envOps(profile).pkgAvailableVersion(pkg);
1731
+ if (!read || !probe.supported) return null;
1732
+ const out = await tryExec(executor, opScript(probe.value));
1733
+ return out ? read(out) : null;
1887
1734
  }
1888
1735
  async function enrichAvailableVersions(executor, profile, statuses) {
1889
1736
  const targets = statuses.filter(
@@ -1893,7 +1740,7 @@ async function enrichAvailableVersions(executor, profile, statuses) {
1893
1740
  targets.map(async (s) => {
1894
1741
  try {
1895
1742
  const pkg = PACKAGE_NAMES[s.name];
1896
- const candidate = await probeCandidate(executor, profile.packageManager, pkg);
1743
+ const candidate = await probeCandidate(executor, profile, pkg);
1897
1744
  if (!candidate) return;
1898
1745
  const avail = normalizePkgVersion(candidate);
1899
1746
  const installed = normalizePkgVersion(s.version);
@@ -1957,37 +1804,8 @@ function getSystemComponentDefinition(name) {
1957
1804
  }
1958
1805
 
1959
1806
  // ../../packages/adapters/src/system/checks.ts
1960
- async function tryExec4(executor, command) {
1961
- const startedAt = Date.now();
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}`;
1807
+ function isSocketDenied(error) {
1808
+ return error != null && /permission denied|eacces/i.test(error);
1991
1809
  }
1992
1810
  function healthy(name, version, running) {
1993
1811
  const component = getSystemComponentDefinition(name);
@@ -2020,22 +1838,31 @@ function unhealthy(name, message, opts) {
2020
1838
  async function checkDocker(executor) {
2021
1839
  const startedAt = Date.now();
2022
1840
  const recipe = systemCatalog.checks.docker;
2023
- const version = await tryExec4(executor, recipe.versionCommand);
1841
+ const version = await probeExec(executor, recipe.versionCommand, "checks");
2024
1842
  if (!version.output) {
2025
1843
  systemDebug("checks", `docker:missing (${formatDuration(startedAt)})`);
2026
1844
  return unhealthy("docker", withReason(recipe.missingMessage, version.error));
2027
1845
  }
2028
1846
  const parsed = recipe.parseVersion(version.output);
2029
- const info = await tryExec4(executor, recipe.daemonCommand);
1847
+ const info = await probeExec(executor, recipe.daemonCommand, "checks");
2030
1848
  if (!info.output) {
1849
+ const denied = isSocketDenied(info.error);
2031
1850
  systemDebug(
2032
1851
  "checks",
2033
- `docker:not-running (${formatDuration(startedAt)}) ${info.error ?? "probe exited 0 with no server version"}`
1852
+ `docker:${denied ? "denied" : "not-running"} (${formatDuration(startedAt)}) ${info.error ?? "probe exited 0 with no server version"}`
1853
+ );
1854
+ return unhealthy(
1855
+ "docker",
1856
+ withReason(denied && recipe.deniedMessage || recipe.notRunningMessage, info.error),
1857
+ {
1858
+ version: parsed,
1859
+ // Left UNSET when denied: `running: false` would be a fact we don't have. The
1860
+ // permission check happens before the daemon is consulted, so a refusal is
1861
+ // equally consistent with a healthy daemon, and asserting it is stopped is the
1862
+ // same class of guess the message above stopped making.
1863
+ ...denied ? {} : { running: false }
1864
+ }
2034
1865
  );
2035
- return unhealthy("docker", withReason(recipe.notRunningMessage, info.error), {
2036
- version: parsed,
2037
- running: false
2038
- });
2039
1866
  }
2040
1867
  systemDebug("checks", `docker:healthy (${formatDuration(startedAt)})`);
2041
1868
  return healthy("docker", parsed, true);
@@ -2043,7 +1870,7 @@ async function checkDocker(executor) {
2043
1870
  async function checkGit(executor) {
2044
1871
  const startedAt = Date.now();
2045
1872
  const recipe = systemCatalog.checks.git;
2046
- const version = await tryExec4(executor, recipe.versionCommand);
1873
+ const version = await probeExec(executor, recipe.versionCommand, "checks");
2047
1874
  if (!version.output) {
2048
1875
  systemDebug("checks", `git:missing (${formatDuration(startedAt)})`);
2049
1876
  return unhealthy("git", withReason(recipe.missingMessage, version.error));
@@ -2055,7 +1882,7 @@ async function checkGit(executor) {
2055
1882
  async function checkRsync(executor) {
2056
1883
  const startedAt = Date.now();
2057
1884
  const recipe = systemCatalog.checks.rsync;
2058
- const version = await tryExec4(executor, recipe.versionCommand);
1885
+ const version = await probeExec(executor, recipe.versionCommand, "checks");
2059
1886
  if (!version.output) {
2060
1887
  systemDebug("checks", `rsync:missing (${formatDuration(startedAt)})`);
2061
1888
  return unhealthy("rsync", withReason(recipe.missingMessage, version.error));
@@ -2083,7 +1910,7 @@ async function checkEdge(executor) {
2083
1910
  { running: false }
2084
1911
  );
2085
1912
  }
2086
- const version = await tryExec4(executor, containerCommand(container, "openresty -v 2>&1"));
1913
+ const version = await probeExec(executor, containerCommand(container, "openresty -v 2>&1"), "checks");
2087
1914
  if (!version.output) {
2088
1915
  systemDebug("checks", `edge:container-unresponsive (${formatDuration(startedAt)})`);
2089
1916
  return unhealthy(
@@ -2159,6 +1986,182 @@ async function checkComponents(executor, names) {
2159
1986
  return results;
2160
1987
  }
2161
1988
 
1989
+ // ../../packages/adapters/src/system/installer.ts
1990
+ function log3(message, level = "info") {
1991
+ return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
1992
+ }
1993
+ async function prepareExecutor(executor, component) {
1994
+ const grant = await privilegedExecutor(executor, `Installing ${component}`);
1995
+ if (!grant.supported) {
1996
+ return { ok: false, result: { component, success: false, error: grant.reason } };
1997
+ }
1998
+ return { ok: true, executor: grant.value.executor, profile: grant.value.profile };
1999
+ }
2000
+ function describeInstallFailure(label, code, output) {
2001
+ const lines = output.split("\n").map((line) => line.replace(/\r/g, "").trim()).filter(Boolean);
2002
+ const flagged = lines.filter((line) => /^(e:|err:|error:|dpkg:|fatal:)/i.test(line));
2003
+ const picked = (flagged.length ? flagged : lines).slice(-3).join(" \xB7 ");
2004
+ const detail = picked.length > 400 ? `${picked.slice(0, 399)}\u2026` : picked;
2005
+ 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." : "";
2006
+ return `${label} install failed (exit ${code})${detail ? `: ${detail}` : ""}.${held}`;
2007
+ }
2008
+ async function probeDocker(executor) {
2009
+ const status = await checkDocker(executor);
2010
+ const version = status.version;
2011
+ if (!status.installed) return { verdict: "missing", message: status.message };
2012
+ if (!status.healthy) return { verdict: "stopped", version, message: status.message };
2013
+ if (version && compareSemver(version, MIN_DOCKER_VERSION) < 0) {
2014
+ return { verdict: "outdated", version, message: status.message };
2015
+ }
2016
+ return { verdict: "ok", version, message: status.message };
2017
+ }
2018
+ function dockerAlreadyThere(present, onLog) {
2019
+ const named = present.version ? `Docker ${present.version}` : "Docker";
2020
+ onLog(log3(`${named} is installed and its daemon is answering \u2014 nothing to install`));
2021
+ return { component: "docker", success: true, version: present.version };
2022
+ }
2023
+ async function installDocker(executor, onLog, config) {
2024
+ const forced = config?.reinstall === true;
2025
+ let present = forced ? null : await probeDocker(executor);
2026
+ if (present?.verdict === "ok") return dockerAlreadyThere(present, onLog);
2027
+ const prep = await prepareExecutor(executor, "docker");
2028
+ if (!prep.ok) return prep.result;
2029
+ executor = prep.executor;
2030
+ const profile = prep.profile;
2031
+ if (present && present.verdict !== "missing" && !profile.isRoot) {
2032
+ present = await probeDocker(executor);
2033
+ if (present.verdict === "ok") return dockerAlreadyThere(present, onLog);
2034
+ }
2035
+ const plan = systemCatalog.installs.docker(profile);
2036
+ const service = plan.supported ? plan.value.service : plan;
2037
+ if (present?.verdict === "stopped" && service?.supported) {
2038
+ onLog(log3(`${present.message} \u2014 starting the Docker service`, "warn"));
2039
+ await executor.streamExec(service.value, onLog).catch(() => void 0);
2040
+ present = await probeDocker(executor);
2041
+ if (present.verdict === "ok") {
2042
+ onLog(log3(`Docker ${present.version} is running`));
2043
+ return { component: "docker", success: true, version: present.version };
2044
+ }
2045
+ }
2046
+ if (present && present.verdict !== "missing") {
2047
+ const cannotStart = present.verdict === "stopped" && service && !service.supported ? ` Openship could not start it for you: ${service.reason}` : "";
2048
+ 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;
2049
+ onLog(log3(error, "error"));
2050
+ return { component: "docker", success: false, version: present.version, error };
2051
+ }
2052
+ if (!plan.supported) {
2053
+ onLog(log3(plan.reason, "error"));
2054
+ return { component: "docker", success: false, error: plan.reason };
2055
+ }
2056
+ onLog(
2057
+ forced ? log3(
2058
+ "Running the Docker installer \u2014 it may upgrade the engine, which restarts the daemon and every container on this host...",
2059
+ "warn"
2060
+ ) : log3("Installing Docker Engine...")
2061
+ );
2062
+ try {
2063
+ const { code, output } = await executor.streamExec(
2064
+ plan.value.installCommand,
2065
+ onLog
2066
+ );
2067
+ if (code !== 0) {
2068
+ const error = describeInstallFailure("Docker", code, output);
2069
+ onLog(log3(error, "error"));
2070
+ return { component: "docker", success: false, error };
2071
+ }
2072
+ if (service?.supported) {
2073
+ onLog(log3("Starting Docker service..."));
2074
+ await executor.streamExec(service.value, onLog);
2075
+ } else if (service) {
2076
+ onLog(log3(`Docker is installed but Openship cannot start it here: ${service.reason}`, "warn"));
2077
+ }
2078
+ const version = await executor.exec(plan.value.verifyCommand);
2079
+ const parsed = systemCatalog.checks.docker.parseVersion(version);
2080
+ onLog(log3(`Docker ${parsed} installed`));
2081
+ return { component: "docker", success: true, version: parsed };
2082
+ } catch (err) {
2083
+ const msg = safeErrorMessage(err);
2084
+ onLog(log3(`Docker installation failed: ${msg}`, "error"));
2085
+ return { component: "docker", success: false, error: msg };
2086
+ }
2087
+ }
2088
+ async function installPackaged(executor, onLog, component, label, where) {
2089
+ const prep = await prepareExecutor(executor, component);
2090
+ if (!prep.ok) return prep.result;
2091
+ executor = prep.executor;
2092
+ const plan = systemCatalog.installs[component](prep.profile);
2093
+ if (!plan.supported) {
2094
+ onLog(log3(plan.reason, "error"));
2095
+ return { component, success: false, error: plan.reason };
2096
+ }
2097
+ const inside = where ? ` in ${where}` : "";
2098
+ onLog(log3(`Installing ${label}${inside}...`));
2099
+ try {
2100
+ const { code, output } = await executor.streamExec(
2101
+ plan.value.installCommand,
2102
+ onLog
2103
+ );
2104
+ if (code !== 0) {
2105
+ const error = describeInstallFailure(label, code, output);
2106
+ onLog(log3(error, "error"));
2107
+ return { component, success: false, error };
2108
+ }
2109
+ const version = await executor.exec(plan.value.verifyCommand);
2110
+ const parsed = systemCatalog.checks[component].parseVersion(version);
2111
+ onLog(log3(`${label} ${parsed} installed${inside}`));
2112
+ return { component, success: true, version: parsed };
2113
+ } catch (err) {
2114
+ const msg = safeErrorMessage(err);
2115
+ onLog(log3(`${label} installation failed: ${msg}`, "error"));
2116
+ return { component, success: false, error: msg };
2117
+ }
2118
+ }
2119
+ function installGit(executor, onLog, opts) {
2120
+ return installPackaged(executor, onLog, "git", "Git", opts?.label);
2121
+ }
2122
+ function installRsync(executor, onLog) {
2123
+ return installPackaged(executor, onLog, "rsync", "rsync");
2124
+ }
2125
+ async function installContainerEdge(executor, onLog, config) {
2126
+ const prep = await prepareExecutor(executor, "edge");
2127
+ if (!prep.ok) return prep.result;
2128
+ executor = prep.executor;
2129
+ if (!await dockerAvailable(executor)) {
2130
+ const error = "The edge is a container image and needs Docker on this server. Install the Docker component first, then install the edge.";
2131
+ onLog(log3(error, "error"));
2132
+ return { component: "edge", success: false, error };
2133
+ }
2134
+ try {
2135
+ const result = await ensureContainerEdge(executor, {
2136
+ onLog,
2137
+ config,
2138
+ image: config?.edgeImage
2139
+ });
2140
+ if (result.converted) {
2141
+ onLog(log3("Edge moved from host OpenResty to the container edge", "warn"));
2142
+ }
2143
+ const version = await executor.exec(containerCommand(result.container, "openresty -v 2>&1")).catch(() => "");
2144
+ return {
2145
+ component: "edge",
2146
+ success: true,
2147
+ version: version ? systemCatalog.checks.openresty.parseVersion(version) : void 0
2148
+ };
2149
+ } catch (err) {
2150
+ if (err instanceof EdgeMigrateRequested) throw err;
2151
+ const msg = safeErrorMessage(err);
2152
+ onLog(log3(`Edge setup failed: ${msg}`, "error"));
2153
+ return { component: "edge", success: false, error: msg };
2154
+ }
2155
+ }
2156
+ var COMPONENT_INSTALLERS = {
2157
+ docker: (exec, log6, config) => installDocker(exec, log6, config),
2158
+ // The edge is ONE component and it is a container image — there is no
2159
+ // host-OpenResty or host-certbot installer to reach for any more.
2160
+ edge: (exec, log6, config) => installContainerEdge(exec, log6, config),
2161
+ git: (exec, log6) => installGit(exec, log6),
2162
+ rsync: (exec, log6) => installRsync(exec, log6)
2163
+ };
2164
+
2162
2165
  // ../../packages/adapters/src/system/proxy/index.ts
2163
2166
  async function importSites(executor, status) {
2164
2167
  const proxy = status.occupants.find((o) => o.proxy)?.proxy;
@@ -2206,8 +2209,12 @@ async function ensureEdgeClear(executor, config, onLog) {
2206
2209
  if (!freed.freed) {
2207
2210
  await rollbackEdgeTakeover(executor, onLog);
2208
2211
  const plural = freed.stillBound.length > 1;
2212
+ 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
2213
  throw new AppError(
2210
- `Stopped the existing proxy, but port${plural ? "s" : ""} ${freed.stillBound.join(" and ")} ${plural ? "are" : "is"} still in use, so the edge can't bind. Nothing was installed and the previous proxy has been restored \u2014 find what else is holding the port and retry.`,
2214
+ // The advice has to match the actual cause. Told to hunt for another holder, an
2215
+ // operator retries the same unprivileged takeover forever — and "stopped the
2216
+ // existing proxy" is untrue on this arm, since the stop is what was refused.
2217
+ 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
2218
  409,
2212
2219
  "EDGE_PORTS_STILL_BOUND"
2213
2220
  );
@@ -2262,15 +2269,22 @@ function setDefaultEdgeImage(image) {
2262
2269
  function resolveEdgeImage(explicit) {
2263
2270
  return buildEdgeImageRef({ explicit, injectedDefault: injectedDefaultImage });
2264
2271
  }
2272
+ function edgeHostExecutor(executor) {
2273
+ return rootOrDegrade(executor, {
2274
+ purpose: "Configuring routing and TLS",
2275
+ 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.",
2276
+ report: (message) => console.error(`[edge] ${message}`)
2277
+ });
2278
+ }
2265
2279
  async function containerEdgeProvider(executor, container, opts) {
2266
- const { NginxProvider: Provider } = await import("./nginx-AXGNWRKZ.js");
2280
+ const { NginxProvider: Provider } = await import("./nginx-NSAHWGJV.js");
2267
2281
  return new Provider({
2268
2282
  ...opts,
2269
2283
  // Vhosts go to the BIND-MOUNTED HOST dir (so every host-side reader — the
2270
2284
  // migrate proxy scan, cert reuse, the mail cert symlinks — keeps working),
2271
2285
  // while reload/certbot run inside the container.
2272
2286
  paths: EDGE_HOST_PATHS,
2273
- executor: edgeContainerExecutor(executor, container),
2287
+ executor: edgeContainerExecutor(await edgeHostExecutor(executor), container),
2274
2288
  // Challenge tokens go to the HOST side of the ACME mount, for the same reason
2275
2289
  // vhosts do. This is the ONE mount whose two sides differ
2276
2290
  // (/var/lib/openship/edge/acme → /var/www/acme), so the write path and the
@@ -2287,12 +2301,21 @@ async function containerEdgeProvider(executor, container, opts) {
2287
2301
  });
2288
2302
  }
2289
2303
  async function localContainerEdgeProvider(container, opts) {
2290
- const { DockerEdgeExecutor } = await import("./docker-edge-executor-62N64MGS.js");
2291
- const { NginxProvider: Provider } = await import("./nginx-AXGNWRKZ.js");
2304
+ const { DockerEdgeExecutor } = await import("./docker-edge-executor-YZZ6P7IC.js");
2305
+ const { NginxProvider: Provider } = await import("./nginx-NSAHWGJV.js");
2292
2306
  return new Provider({
2293
2307
  ...opts,
2294
2308
  paths: OPENRESTY_DEFAULT_PATHS,
2295
- executor: new DockerEdgeExecutor({ containerName: container }),
2309
+ // The one place the privilege gate is the WRONG instrument, so the reason is stated
2310
+ // instead: this executor's commands run inside the EDGE container, so probing
2311
+ // through it would measure that container's interior and brand the result with a
2312
+ // verdict about the wrong machine. Privilege here comes from the deployment shape —
2313
+ // the api process is root in its own container, the vhost dir is a bind mount it
2314
+ // owns, and the daemon socket is mounted in. There is no login to elevate.
2315
+ executor: rootChecked(
2316
+ new DockerEdgeExecutor({ containerName: container }),
2317
+ "compose: api is root in its own container, writing shared mounts over a mounted socket"
2318
+ ),
2296
2319
  // nginx.conf + the Lua are BAKED into the image — nothing to detect, install
2297
2320
  // or patch, and detection would answer from inside the container anyway.
2298
2321
  pinPaths: true,
@@ -2412,7 +2435,13 @@ async function ensureContainerEdge(executor, opts) {
2412
2435
  }
2413
2436
  }
2414
2437
  for (const mount of EDGE_CONTAINER_MOUNTS) {
2415
- await executor.exec(`mkdir -p ${sq(mount.host)}`).catch(() => {
2438
+ await executor.exec(`mkdir -p ${sq(mount.host)}`).catch((err) => {
2439
+ onLog(
2440
+ log5(
2441
+ `Could not create the edge state directory ${mount.host}: ${safeErrorMessage(err)}. Docker will create it empty, so vhosts and certificates may not persist.`,
2442
+ "warn"
2443
+ )
2444
+ );
2416
2445
  });
2417
2446
  }
2418
2447
  const clear = await ensureEdgeClear(executor, opts.config, onLog);
@@ -2472,6 +2501,7 @@ export {
2472
2501
  verifyEdgeServing,
2473
2502
  ensureContainerEdge,
2474
2503
  checkGit,
2504
+ COMPONENT_CHECKS,
2475
2505
  checkAll,
2476
2506
  checkComponents
2477
2507
  };