openship 0.4.7 → 0.5.0

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 (42) hide show
  1. package/README.md +1 -0
  2. package/dist/{bare-2HYDXQEH.js → bare-VRGE5MXH.js} +7 -7
  3. package/dist/{chunk-TGIINPVM.js → chunk-6LKHXB6T.js} +22 -7
  4. package/dist/{chunk-R5BHFFAV.js → chunk-6PIYRG5I.js} +56 -17
  5. package/dist/{chunk-IMBD5PZ5.js → chunk-BS2AXYZX.js} +430 -86
  6. package/dist/{chunk-SJIFLLX4.js → chunk-CUJXGK7R.js} +321 -85
  7. package/dist/{chunk-VFINNTUL.js → chunk-EAAGSQXN.js} +23 -8
  8. package/dist/{chunk-4Z5QO7N6.js → chunk-I6PSTQIR.js} +2 -2
  9. package/dist/{chunk-XFCDYZUQ.js → chunk-JLKKYUZA.js} +31 -49
  10. package/dist/{chunk-4TJC4JZQ.js → chunk-K2RPVGPN.js} +23 -4
  11. package/dist/{chunk-MZ2WEHHY.js → chunk-KNTJ7PSF.js} +1 -1
  12. package/dist/{chunk-L2VOGB2X.js → chunk-KZOBYJUW.js} +1 -1
  13. package/dist/{chunk-IPYPMI3S.js → chunk-NNPEG6HM.js} +1 -1
  14. package/dist/{chunk-UXWAVM4C.js → chunk-NXFIRK2Z.js} +398 -112
  15. package/dist/{chunk-FXTBHTWY.js → chunk-RB35YN3J.js} +271 -35
  16. package/dist/{chunk-6APULWSU.js → chunk-UMQP7SYT.js} +429 -66
  17. package/dist/{chunk-V6AN6XLZ.js → chunk-V7BZMGOT.js} +123 -45
  18. package/dist/{chunk-VDMQJHPI.js → chunk-YWMODKWL.js} +67 -4
  19. package/dist/{chunk-OXWDY22G.js → chunk-ZCOQJCZO.js} +1 -1
  20. package/dist/{cloud-2KXREXMO.js → cloud-3M2WZ624.js} +12 -12
  21. package/dist/{detect-2XSCFFTF.js → detect-BAJMY6KM.js} +7 -5
  22. package/dist/{docker-LODK5Y2S.js → docker-2NGBRJFJ.js} +9 -7
  23. package/dist/{docker-edge-executor-VHW3JPJS.js → docker-edge-executor-5I4M5QT7.js} +1 -1
  24. package/dist/{edge-import-ZQ4Y5HEA.js → edge-import-RIWNEIXQ.js} +21 -19
  25. package/dist/{ensure-container-edge-ED25XL3Z.js → ensure-container-edge-3NSYKHRX.js} +6 -6
  26. package/dist/{executor-YPT6RH6O.js → executor-LOYMPU67.js} +3 -3
  27. package/dist/index.js +309 -338
  28. package/dist/{nginx-XWJAOGS4.js → nginx-QE4DOM5J.js} +4 -4
  29. package/dist/{openresty-lua-4WLGDSER.js → openresty-lua-BTAKECCF.js} +2 -2
  30. package/dist/server/index.js +51705 -45369
  31. package/dist/server/migrations/0073_project_volumes_object_storage.sql +3 -0
  32. package/dist/server/migrations/0074_project_compose_path.sql +1 -0
  33. package/dist/server/migrations/0075_service_command_argv.sql +1 -0
  34. package/dist/server/migrations/0076_rollback_retention.sql +17 -0
  35. package/dist/server/migrations/0077_domain_redirect.sql +13 -0
  36. package/dist/server/migrations/0078_grant_source_scope.sql +30 -0
  37. package/dist/server/migrations/0079_project_readiness.sql +18 -0
  38. package/dist/server/migrations/0080_deployment_error_classification.sql +26 -0
  39. package/dist/server/migrations/0081_project_readiness_repair.sql +34 -0
  40. package/dist/server/migrations/meta/_journal.json +63 -0
  41. package/dist/{setup-2DMFIA74.js → setup-KELALK7H.js} +7 -7
  42. package/package.json +1 -1
@@ -4,7 +4,7 @@ import { createRequire as __ospCreateRequire } from "node:module";
4
4
  const require = __ospCreateRequire(import.meta.url);
5
5
  import {
6
6
  resolveEnvironment
7
- } from "./chunk-L2VOGB2X.js";
7
+ } from "./chunk-KZOBYJUW.js";
8
8
  import {
9
9
  formatDuration,
10
10
  isRemoteConnectionError,
@@ -17,7 +17,7 @@ import {
17
17
  readDeclaredPair,
18
18
  readMaybeInContainer,
19
19
  validateCertFor
20
- } from "./chunk-FXTBHTWY.js";
20
+ } from "./chunk-RB35YN3J.js";
21
21
  import {
22
22
  sq
23
23
  } from "./chunk-FPRHYBY2.js";
@@ -31,9 +31,10 @@ import {
31
31
  ourLuaOnHost,
32
32
  probeEdge,
33
33
  resolveOurEdgeContainer,
34
+ sanitizeEdgeVhosts,
34
35
  sq as sq2,
35
36
  stopTargetsForStatus
36
- } from "./chunk-R5BHFFAV.js";
37
+ } from "./chunk-6PIYRG5I.js";
37
38
  import {
38
39
  waitForPortListening
39
40
  } from "./chunk-ILFETZOM.js";
@@ -42,12 +43,12 @@ import {
42
43
  EDGE_HOST_PATHS,
43
44
  OPENRESTY_DEFAULT_PATHS,
44
45
  detectOpenRestyPaths
45
- } from "./chunk-VFINNTUL.js";
46
+ } from "./chunk-EAAGSQXN.js";
46
47
  import {
47
48
  buildEdgeImageRef,
48
49
  compareSemver,
49
50
  safeErrorMessage
50
- } from "./chunk-6APULWSU.js";
51
+ } from "./chunk-UMQP7SYT.js";
51
52
 
52
53
  // ../../packages/adapters/src/system/proxy/import/parse-utils.ts
53
54
  async function tryExec(executor, command) {
@@ -81,6 +82,24 @@ function extractBlocks(text, keyword) {
81
82
  }
82
83
  return blocks;
83
84
  }
85
+ function collapseByHost(items, hostsOf, score) {
86
+ const groups = /* @__PURE__ */ new Map();
87
+ for (const item of items) {
88
+ const key = [...hostsOf(item)].sort().join(" ");
89
+ const bag = groups.get(key);
90
+ if (bag) bag.push(item);
91
+ else groups.set(key, [item]);
92
+ }
93
+ const kept = [];
94
+ const dropped = [];
95
+ for (const bag of groups.values()) {
96
+ let winner = bag[0];
97
+ for (const cand of bag) if (score(cand) > score(winner)) winner = cand;
98
+ kept.push(winner);
99
+ for (const loser of bag) if (loser !== winner) dropped.push(loser);
100
+ }
101
+ return { kept, dropped };
102
+ }
84
103
  function sq3(value) {
85
104
  return `'${value.replace(/'/g, "'\\''")}'`;
86
105
  }
@@ -121,7 +140,10 @@ function resolveProxyTarget(proxyPass, upstreams) {
121
140
  const host = authority.replace(/:\d+$/, "");
122
141
  if (upstreams.has(host)) return { url: `${scheme}${upstreams.get(host)}` };
123
142
  const isIp = /^\d{1,3}(\.\d{1,3}){3}$/.test(host);
124
- if (isIp || host === "localhost" || host.includes(".")) return { url: raw };
143
+ if (host === "localhost" || host === "ip6-localhost") {
144
+ return { url: raw.replace(/^(https?:\/\/)[^/:]+/i, "$1127.0.0.1") };
145
+ }
146
+ if (isIp || host.includes(".")) return { url: raw };
125
147
  return { reason: `proxy_pass host "${host}" is an undeclared upstream \u2014 not migratable` };
126
148
  }
127
149
  function extractLocationProxies(serverBody) {
@@ -159,6 +181,16 @@ function isHttpsUpgradeForSelf(body, names) {
159
181
  return names.includes(host);
160
182
  });
161
183
  }
184
+ function isAcmeWebrootOnly(body) {
185
+ const paths = [...body.matchAll(/(?:^|[\s;}])location\s+([^{]+?)\s*\{/g)].map(
186
+ (m) => m[1].trim()
187
+ );
188
+ if (paths.length === 0) {
189
+ const root = firstDirective(body, "root") ?? "";
190
+ return /letsencrypt|acme|certbot/i.test(root);
191
+ }
192
+ return paths.every((p) => /\.well-known\/acme-challenge/i.test(p));
193
+ }
162
194
  function parseServer(body, source, upstreams) {
163
195
  const warnings = [];
164
196
  const names = firstDirective(body, "server_name")?.split(/\s+/).filter((n) => n && n !== "_" && !n.startsWith("~")) ?? [];
@@ -185,10 +217,12 @@ function parseServer(body, source, upstreams) {
185
217
  const primary = resolved.find((r) => r.path === "/") ?? resolved[0];
186
218
  target = { kind: "proxy", url: primary.url };
187
219
  routes = resolved;
188
- } else if (root) {
189
- target = { kind: "static", root: root.replace(/;$/, "") };
190
220
  } else if (isHttpsUpgradeForSelf(body, names)) {
191
221
  return { warnings: [] };
222
+ } else if (root && !isAcmeWebrootOnly(body)) {
223
+ target = { kind: "static", root: root.replace(/;$/, "") };
224
+ } else if (root) {
225
+ return { warnings: [] };
192
226
  } else {
193
227
  warnings.push(`nginx: ${names[0]} has neither proxy_pass nor root \u2014 skipped (${source})`);
194
228
  return { warnings };
@@ -215,7 +249,12 @@ function parseNginxConfig(raw) {
215
249
  warnings.push(...blockWarnings);
216
250
  if (site) sites.push(site);
217
251
  }
218
- return { proxy: "nginx", sites, warnings };
252
+ const { kept } = collapseByHost(
253
+ sites,
254
+ (s) => s.serverNames,
255
+ (s) => (s.ssl ? 2 : 0) + (s.target.kind === "proxy" ? 1 : 0)
256
+ );
257
+ return { proxy: "nginx", sites: kept, warnings };
219
258
  }
220
259
  async function scanNginx(executor) {
221
260
  return parseNginxConfig(await loadNginxConfig(executor));
@@ -244,23 +283,61 @@ async function loadCaddyJson(executor) {
244
283
  }
245
284
  return null;
246
285
  }
247
- function flattenHandlers(handlers, out = []) {
286
+ function flattenHandlers(handlers, scoped = false, out = []) {
248
287
  for (const h of handlers ?? []) {
249
288
  if (h.handler === "subroute") {
250
- for (const r of h.routes ?? []) flattenHandlers(r.handle, out);
289
+ for (const r of h.routes ?? []) {
290
+ flattenHandlers(r.handle, scoped || (r.match?.length ?? 0) > 0, out);
291
+ }
251
292
  } else {
252
- out.push(h);
293
+ out.push({ h, scoped });
253
294
  }
254
295
  }
255
296
  return out;
256
297
  }
298
+ var UNMIGRATABLE_HANDLERS = {
299
+ php_fastcgi: "a PHP-FPM site (php_fastcgi)",
300
+ fastcgi: "a FastCGI backend",
301
+ static_response: "a static inline response (respond)",
302
+ templates: "Caddy templates",
303
+ encode: "",
304
+ headers: "",
305
+ authentication: "Caddy authentication",
306
+ rewrite: "",
307
+ acme_server: "an embedded ACME server"
308
+ };
257
309
  function targetFromRoute(route) {
258
310
  const flat = flattenHandlers(route.handle);
259
- const dial = flat.find((h) => h.handler === "reverse_proxy")?.upstreams?.find((u) => u.dial)?.dial;
260
- if (dial) return { kind: "proxy", url: /^https?:\/\//.test(dial) ? dial : `http://${dial}` };
261
- const root = flat.find((h) => h.handler === "vars" && typeof h.root === "string")?.root;
262
- if (root && flat.some((h) => h.handler === "file_server")) return { kind: "static", root };
263
- return null;
311
+ const proxies = flat.filter((f) => f.h.handler === "reverse_proxy");
312
+ const chosen = proxies.find((f) => !f.scoped) ?? proxies[0];
313
+ const proxy = chosen?.h;
314
+ if (proxy?.transport?.protocol === "fastcgi") {
315
+ return { note: "serves a PHP-FPM/FastCGI site (php_fastcgi) \u2014 not an HTTP upstream" };
316
+ }
317
+ const pathScopedOnly = Boolean(chosen?.scoped);
318
+ const hasScopedSibling = proxies.some((f) => f.scoped);
319
+ const dials = (proxy?.upstreams ?? []).map((u) => u.dial).filter((d) => Boolean(d));
320
+ if (dials.length > 0) {
321
+ const dial = dials[0];
322
+ if (/^unix\//i.test(dial)) {
323
+ return { note: `proxies to the unix socket "${dial}", which needs a manual upstream` };
324
+ }
325
+ return {
326
+ target: { kind: "proxy", url: /^https?:\/\//.test(dial) ? dial : `http://${dial}` },
327
+ extraUpstreams: dials.length - 1,
328
+ pathScopedOnly,
329
+ hasScopedSibling
330
+ };
331
+ }
332
+ const root = flat.find((f) => f.h.handler === "vars" && typeof f.h.root === "string")?.h.root;
333
+ if (root && flat.some((f) => f.h.handler === "file_server")) {
334
+ return { target: { kind: "static", root } };
335
+ }
336
+ for (const { h } of flat) {
337
+ const label = h.handler ? UNMIGRATABLE_HANDLERS[h.handler] : void 0;
338
+ if (label) return { note: `serves ${label}` };
339
+ }
340
+ return {};
264
341
  }
265
342
  function parseCaddyJson(raw) {
266
343
  let cfg;
@@ -272,17 +349,47 @@ function parseCaddyJson(raw) {
272
349
  const servers = cfg.apps?.http?.servers;
273
350
  if (!servers) return null;
274
351
  const sites = [];
352
+ const warnings = [];
275
353
  for (const srv of Object.values(servers)) {
276
354
  const ssl = (srv.listen ?? []).some((l) => l.endsWith(":443"));
277
355
  for (const route of srv.routes ?? []) {
278
356
  const hosts = (route.match ?? []).flatMap((m) => m.host ?? []).filter((h) => h && h !== "*");
279
357
  if (hosts.length === 0) continue;
280
- const target = targetFromRoute(route);
281
- if (!target) continue;
282
- sites.push({ serverNames: hosts, ssl, target, source: "caddy (adapt)" });
358
+ const outcome = targetFromRoute(route);
359
+ if (outcome.note) {
360
+ warnings.push(`caddy: ${hosts.join(", ")} ${outcome.note} \u2014 re-add it manually`);
361
+ continue;
362
+ }
363
+ if (!outcome.target) continue;
364
+ if (outcome.extraUpstreams) {
365
+ warnings.push(
366
+ `caddy: ${hosts.join(", ")} load-balances across ${outcome.extraUpstreams + 1} upstreams \u2014 only the first is migrated`
367
+ );
368
+ }
369
+ if (outcome.hasScopedSibling || (route.match ?? []).some((m) => m.path?.length)) {
370
+ warnings.push(
371
+ `caddy: ${hosts.join(", ")} routes some paths to other upstreams \u2014 only the catch-all is migrated; re-add path rules manually`
372
+ );
373
+ }
374
+ if (outcome.pathScopedOnly) {
375
+ warnings.push(
376
+ `caddy: ${hosts.join(", ")} has no catch-all upstream \u2014 used its path-scoped one; verify the target`
377
+ );
378
+ }
379
+ sites.push({ serverNames: hosts, ssl, target: outcome.target, source: "caddy (adapt)" });
283
380
  }
284
381
  }
285
- return { sites, warnings: [] };
382
+ const { kept, dropped } = collapseByHost(
383
+ sites,
384
+ (s) => s.serverNames,
385
+ (s) => s.ssl ? 1 : 0
386
+ );
387
+ for (const d of dropped) {
388
+ warnings.push(
389
+ `caddy: ${d.serverNames.join(", ")} had a second (non-TLS) block \u2014 kept the TLS one`
390
+ );
391
+ }
392
+ return { sites: kept, warnings };
286
393
  }
287
394
  async function loadCaddyfile(executor) {
288
395
  for (const p of CADDYFILE_PATHS) {
@@ -326,7 +433,7 @@ async function scanCaddy(executor) {
326
433
  const json = await loadCaddyJson(executor);
327
434
  if (json) {
328
435
  const parsed = parseCaddyJson(json);
329
- if (parsed && parsed.sites.length > 0) {
436
+ if (parsed && (parsed.sites.length > 0 || parsed.warnings.length > 0)) {
330
437
  return { proxy: "caddy", sites: parsed.sites, warnings: parsed.warnings };
331
438
  }
332
439
  }
@@ -356,19 +463,44 @@ async function scanCaddyText(executor) {
356
463
  warnings.push(`caddy: skipped a block with no usable address (${header.slice(0, 40)})`);
357
464
  continue;
358
465
  }
359
- const proxyMatch = body.match(/(?:^|\s)reverse_proxy\s+([^\n{]+)/);
466
+ const inlineProxy = body.match(/(?:^|\s)reverse_proxy\s+([^\n{]+)/);
467
+ const blockProxy = body.match(/(?:^|\s)reverse_proxy\s*\{([\s\S]*?)\}/);
468
+ const blockTo = blockProxy?.[1].match(/(?:^|\n)\s*to\s+([^\n]+)/);
360
469
  const rootMatch = body.match(/(?:^|\s)root\s+\*?\s*([^\n\s]+)/);
361
470
  const tlsMatch = body.match(/(?:^|\s)tls\s+(\S+)\s+(\S+)/);
471
+ const upstreams = (inlineProxy?.[1] ?? blockTo?.[1] ?? "").trim().split(/\s+/).filter(Boolean);
472
+ const host = addrs[0].host;
362
473
  let target;
363
- if (proxyMatch) {
364
- const upstream = proxyMatch[1].trim().split(/\s+/)[0];
474
+ if (upstreams.length > 0) {
475
+ const upstream = upstreams[0];
476
+ if (/^unix\//i.test(upstream)) {
477
+ warnings.push(
478
+ `caddy: ${host} proxies to the unix socket "${upstream}" \u2014 needs a manual upstream, skipped`
479
+ );
480
+ continue;
481
+ }
482
+ if (upstreams.length > 1) {
483
+ warnings.push(
484
+ `caddy: ${host} load-balances across ${upstreams.length} upstreams \u2014 only the first is migrated`
485
+ );
486
+ }
365
487
  target = { kind: "proxy", url: /^https?:\/\//.test(upstream) ? upstream : `http://${upstream}` };
488
+ } else if (/(?:^|\s)php_fastcgi\s/.test(body)) {
489
+ warnings.push(
490
+ `caddy: ${host} serves a PHP-FPM site (php_fastcgi) \u2014 not migratable as a static root; re-add it manually`
491
+ );
492
+ continue;
366
493
  } else if (rootMatch) {
367
494
  target = { kind: "static", root: rootMatch[1] };
368
495
  } else {
369
- warnings.push(`caddy: ${addrs[0].host} has no reverse_proxy or root \u2014 skipped`);
496
+ warnings.push(`caddy: ${host} has no reverse_proxy or root \u2014 skipped`);
370
497
  continue;
371
498
  }
499
+ if (/(?:^|\s)(handle_path|handle)\s+\//.test(body)) {
500
+ warnings.push(
501
+ `caddy: ${host} routes by path (handle/handle_path) \u2014 only one upstream is migrated; re-add path rules manually`
502
+ );
503
+ }
372
504
  const site = {
373
505
  serverNames: addrs.map((a) => a.host),
374
506
  ssl: addrs.some((a) => a.ssl),
@@ -380,7 +512,17 @@ async function scanCaddyText(executor) {
380
512
  }
381
513
  sites.push(site);
382
514
  }
383
- return { proxy: "caddy", sites, warnings };
515
+ const { kept, dropped } = collapseByHost(
516
+ sites,
517
+ (s) => s.serverNames,
518
+ (s) => s.ssl ? 1 : 0
519
+ );
520
+ for (const d of dropped) {
521
+ warnings.push(
522
+ `caddy: ${d.serverNames.join(", ")} had a second (non-TLS) block \u2014 kept the TLS one`
523
+ );
524
+ }
525
+ return { proxy: "caddy", sites: kept, warnings };
384
526
  }
385
527
 
386
528
  // ../../packages/adapters/src/system/proxy/import/apache.ts
@@ -418,24 +560,51 @@ function directiveAll(body, name) {
418
560
  (m) => m[1].trim()
419
561
  );
420
562
  }
563
+ function isHttpsRedirectOnly(body) {
564
+ const hasProxy = /(?:^|\n)\s*ProxyPass\s+\S+\s+(?!!)\S/i.test(body);
565
+ if (hasProxy) return false;
566
+ const redirects = [
567
+ ...body.matchAll(/(?:^|\n)\s*Redirect(?:Permanent|Match)?\s+(?:\d{3}\s+|permanent\s+|temp\s+)?\S*\s*(\S+)/gi),
568
+ ...body.matchAll(/(?:^|\n)\s*RewriteRule\s+\S+\s+(\S+)\s*\[[^\]]*R=?3\d\d[^\]]*\]/gi)
569
+ ].map((m) => m[1]);
570
+ return redirects.length > 0 && redirects.some((t) => /^https:\/\//i.test(t));
571
+ }
572
+ function hostOnly(name) {
573
+ return name.trim().replace(/^https?:\/\//i, "").replace(/:\d+$/, "");
574
+ }
421
575
  function parseVhost(body, portHint) {
422
576
  const serverName = directive(body, "ServerName")?.split(/\s+/)[0];
423
577
  const aliases = directiveAll(body, "ServerAlias").flatMap((line) => line.split(/\s+/));
424
- const names = [serverName, ...aliases].filter((n) => Boolean(n));
578
+ const names = [serverName, ...aliases].filter((n) => Boolean(n)).map(hostOnly).filter(Boolean);
425
579
  if (names.length === 0) {
426
580
  return { warning: "apache: skipped a VirtualHost with no ServerName" };
427
581
  }
582
+ if (isHttpsRedirectOnly(body)) return null;
428
583
  const routes = [...body.matchAll(/(?:^|\n)\s*ProxyPass\s+(\S+)\s+(\S+)/gi)].map((m) => ({ path: m[1], url: m[2] })).filter((p) => p.url && p.url !== "!");
429
584
  const docRoot = directive(body, "DocumentRoot")?.replace(/^["']|["']$/g, "");
585
+ const fcgiMatch = /(?:^|\n)\s*ProxyPassMatch\s+[^\n]*(fcgi:|php-fpm|php\d*-fpm|\.sock)/i.test(body);
430
586
  const certPath = directive(body, "SSLCertificateFile");
431
587
  const keyPath = directive(body, "SSLCertificateKeyFile");
432
588
  const ssl = /:443/.test(portHint) || /SSLEngine\s+on/i.test(body) || Boolean(certPath);
433
589
  let target;
434
590
  if (routes.length > 0) {
435
591
  const primary = routes.find((r) => r.path === "/") ?? routes[0];
592
+ if (/^balancer:\/\//i.test(primary.url)) {
593
+ return {
594
+ warning: `apache: ${names[0]} proxies to an mod_proxy_balancer cluster (${primary.url}) \u2014 re-add its members manually`
595
+ };
596
+ }
436
597
  target = { kind: "proxy", url: primary.url };
598
+ } else if (fcgiMatch) {
599
+ return {
600
+ warning: `apache: ${names[0]} is a PHP-FPM site (ProxyPassMatch \u2192 FastCGI) \u2014 not migratable as a static root; re-add it manually`
601
+ };
437
602
  } else if (docRoot) {
438
603
  target = { kind: "static", root: docRoot };
604
+ } else if (/(?:^|\n)\s*ProxyPassMatch\s/i.test(body)) {
605
+ return {
606
+ warning: `apache: ${names[0]} uses ProxyPassMatch (regex proxying) \u2014 re-add it manually`
607
+ };
439
608
  } else {
440
609
  return { warning: `apache: ${names[0]} has neither ProxyPass nor DocumentRoot \u2014 skipped` };
441
610
  }
@@ -444,6 +613,11 @@ function parseVhost(body, portHint) {
444
613
  if (certPath && keyPath) site.tls = { certPath, keyPath };
445
614
  return site;
446
615
  }
616
+ function redirectVhostNames(body) {
617
+ const serverName = directive(body, "ServerName")?.split(/\s+/)[0];
618
+ const aliases = directiveAll(body, "ServerAlias").flatMap((l) => l.split(/\s+/));
619
+ return [serverName, ...aliases].filter((n) => Boolean(n)).map(hostOnly).filter(Boolean);
620
+ }
447
621
  function vhostBlocks(text) {
448
622
  const out = [];
449
623
  const re = /<VirtualHost([^>]*)>([\s\S]*?)<\/VirtualHost>/gi;
@@ -463,15 +637,44 @@ async function scanApache(executor) {
463
637
  const text = stripComments(raw);
464
638
  const blocks = vhostBlocks(text);
465
639
  if (blocks.length === 0) warnings.push("apache: no <VirtualHost> blocks found");
640
+ const redirectOnlyHosts = [];
466
641
  for (const { args, body } of blocks) {
467
642
  const parsed = parseVhost(body, args);
643
+ if (parsed === null) {
644
+ redirectOnlyHosts.push(redirectVhostNames(body));
645
+ continue;
646
+ }
468
647
  if ("warning" in parsed) warnings.push(parsed.warning);
469
648
  else sites.push(parsed);
470
649
  }
471
- return { proxy: "apache", sites, warnings };
650
+ const served = new Set(sites.flatMap((s) => s.serverNames));
651
+ for (const names of redirectOnlyHosts) {
652
+ const orphaned = names.filter((n) => !served.has(n));
653
+ if (orphaned.length > 0) {
654
+ warnings.push(
655
+ `apache: ${orphaned.join(", ")} only redirected to HTTPS and has no TLS VirtualHost here \u2014 nothing to serve it after migration`
656
+ );
657
+ }
658
+ }
659
+ const { kept, dropped } = collapseByHost(
660
+ sites,
661
+ (s) => s.serverNames,
662
+ (s) => (s.ssl ? 2 : 0) + (s.target.kind === "proxy" ? 1 : 0)
663
+ );
664
+ for (const d of dropped) {
665
+ warnings.push(
666
+ `apache: ${d.serverNames.join(", ")} had a second VirtualHost (${d.ssl ? "TLS" : "plain HTTP"}, ${d.target.kind}) \u2014 kept the TLS/proxy one`
667
+ );
668
+ }
669
+ return { proxy: "apache", sites: kept, warnings };
472
670
  }
473
671
 
474
672
  // ../../packages/adapters/src/system/proxy/import/traefik.ts
673
+ function lowerKeys(labels) {
674
+ const out = {};
675
+ for (const [k, v] of Object.entries(labels)) out[k.toLowerCase()] = v;
676
+ return out;
677
+ }
475
678
  function extractHosts(rule) {
476
679
  const hosts = [];
477
680
  for (const call of rule.matchAll(/\bHost\(([^)]*)\)/gi)) {
@@ -480,7 +683,7 @@ function extractHosts(rule) {
480
683
  if (h) hosts.push(h);
481
684
  }
482
685
  }
483
- return hosts;
686
+ return [...new Set(hosts)];
484
687
  }
485
688
  function extraMatchers(rule) {
486
689
  const found = /* @__PURE__ */ new Set();
@@ -491,23 +694,59 @@ function extraMatchers(rule) {
491
694
  }
492
695
  return [...found];
493
696
  }
697
+ function collectDefinitions(containers) {
698
+ const services = /* @__PURE__ */ new Map();
699
+ const redirectMiddlewares = /* @__PURE__ */ new Set();
700
+ const streamRouters = [];
701
+ let fileProvider = false;
702
+ let exposedByDefault = true;
703
+ for (const c of containers) {
704
+ const labels = lowerKeys(c.labels);
705
+ for (const [k, v] of Object.entries(labels)) {
706
+ let m = k.match(/^traefik\.http\.services\.([^.]+)\.loadbalancer\.server\.(port|scheme)$/);
707
+ if (m) {
708
+ const def = services.get(m[1]) ?? {};
709
+ if (m[2] === "port") def.port = v;
710
+ else def.scheme = v;
711
+ services.set(m[1], def);
712
+ continue;
713
+ }
714
+ m = k.match(/^traefik\.http\.middlewares\.([^.]+)\.redirectscheme\./);
715
+ if (m) {
716
+ redirectMiddlewares.add(m[1]);
717
+ continue;
718
+ }
719
+ m = k.match(/^traefik\.(tcp|udp)\.routers\.([^.]+)\.rule$/);
720
+ if (m) streamRouters.push(`${m[1]} router "${m[2]}"`);
721
+ }
722
+ const cmd = c.cmd ?? "";
723
+ if (/--?providers\.file/i.test(cmd)) fileProvider = true;
724
+ if (/--?providers\.docker\.exposedbydefault[= ]?false/i.test(cmd)) exposedByDefault = false;
725
+ }
726
+ return { services, redirectMiddlewares, streamRouters, fileProvider, exposedByDefault };
727
+ }
728
+ function resolveIp(c, labels) {
729
+ const wanted = labels["traefik.docker.network"];
730
+ if (wanted && c.networks?.[wanted]) return c.networks[wanted];
731
+ if (c.ip) return c.ip;
732
+ const first = Object.values(c.networks ?? {}).find(Boolean);
733
+ return first || void 0;
734
+ }
494
735
  function parseTraefikLabels(containers) {
495
- const sites = [];
496
736
  const warnings = [];
737
+ const defs = collectDefinitions(containers);
738
+ const candidates = [];
497
739
  for (const c of containers) {
498
- if (c.labels["traefik.enable"] === "false") continue;
740
+ const labels = lowerKeys(c.labels);
741
+ if (labels["traefik.enable"] === "false") continue;
742
+ if (!defs.exposedByDefault && labels["traefik.enable"] !== "true") continue;
499
743
  const routers = /* @__PURE__ */ new Map();
500
- const servicePorts = /* @__PURE__ */ new Map();
501
- for (const [key, val] of Object.entries(c.labels)) {
744
+ for (const [key, val] of Object.entries(labels)) {
502
745
  const r = key.match(/^traefik\.http\.routers\.([^.]+)\.(.+)$/);
503
- if (r) {
504
- const bag = routers.get(r[1]) ?? {};
505
- bag[r[2]] = val;
506
- routers.set(r[1], bag);
507
- continue;
508
- }
509
- const s = key.match(/^traefik\.http\.services\.([^.]+)\.loadbalancer\.server\.port$/);
510
- if (s) servicePorts.set(s[1], val);
746
+ if (!r) continue;
747
+ const bag = routers.get(r[1]) ?? {};
748
+ bag[r[2]] = val;
749
+ routers.set(r[1], bag);
511
750
  }
512
751
  for (const [rname, props] of routers) {
513
752
  const rule = props.rule;
@@ -519,47 +758,92 @@ function parseTraefikLabels(containers) {
519
758
  );
520
759
  continue;
521
760
  }
522
- if (!c.ip) {
761
+ const ip = resolveIp(c, labels);
762
+ if (!ip) {
523
763
  warnings.push(
524
764
  `traefik: ${hosts.join(", ")} \u2014 couldn't resolve container ${c.name}'s IP; re-add the upstream manually`
525
765
  );
526
766
  continue;
527
767
  }
528
- const svc = props.service;
529
- const rawPort = (svc && servicePorts.get(svc)) ?? (servicePorts.size === 1 ? [...servicePorts.values()][0] : void 0) ?? "80";
768
+ const svc = props.service?.replace(/@.*$/, "");
769
+ const def = (svc ? defs.services.get(svc) : void 0) ?? (defs.services.size === 1 ? [...defs.services.values()][0] : void 0);
770
+ const onlyExposed = c.exposedPorts?.length === 1 ? c.exposedPorts[0].split("/")[0] : void 0;
771
+ const rawPort = def?.port ?? onlyExposed;
530
772
  const port = /^\d{1,5}$/.test(String(rawPort)) ? String(rawPort) : "80";
531
- const url = `http://${c.ip}:${port}`;
532
- const extras = extraMatchers(rule);
533
- if (extras.length > 0) {
534
- warnings.push(
535
- `traefik: ${hosts.join(", ")} also matches ${extras.join(", ")} \u2014 only the Host() part is migrated; re-add path/header rules manually`
536
- );
537
- }
538
- if (props.middlewares) {
539
- warnings.push(`traefik: ${hosts.join(", ")} uses middleware(s) "${props.middlewares}" \u2014 not migrated`);
540
- }
541
- const ssl = Object.keys(props).some((p) => p === "tls" || p.startsWith("tls."));
542
- sites.push({
543
- serverNames: hosts,
544
- ssl,
545
- target: { kind: "proxy", url },
546
- source: `traefik container ${c.name}`
773
+ const scheme = def?.scheme?.toLowerCase() === "https" ? "https" : "http";
774
+ const middlewares = (props.middlewares ?? "").split(",").map((m) => m.trim().replace(/@.*$/, "")).filter(Boolean);
775
+ candidates.push({
776
+ hosts,
777
+ ssl: Object.keys(props).some((p) => p === "tls" || p.startsWith("tls.")),
778
+ url: `${scheme}://${ip}:${port}`,
779
+ container: c.name,
780
+ router: rname,
781
+ rule,
782
+ middlewares,
783
+ redirectOnly: middlewares.length > 0 && middlewares.every((m) => defs.redirectMiddlewares.has(m)),
784
+ portGuessed: rawPort === void 0
547
785
  });
548
786
  }
549
787
  }
550
- return { proxy: "traefik", sites, warnings };
788
+ const { kept: sites, dropped } = collapseByHost(
789
+ candidates,
790
+ (c) => c.hosts,
791
+ (c) => c.redirectOnly ? 0 : c.ssl ? 2 : 1
792
+ );
793
+ for (const cand of sites) {
794
+ const extras = extraMatchers(cand.rule);
795
+ if (extras.length > 0) {
796
+ warnings.push(
797
+ `traefik: ${cand.hosts.join(", ")} also matches ${extras.join(", ")} \u2014 only the Host() part is migrated; re-add path/header rules manually`
798
+ );
799
+ }
800
+ const carried = cand.middlewares.filter((m) => !defs.redirectMiddlewares.has(m));
801
+ if (carried.length > 0) {
802
+ warnings.push(
803
+ `traefik: ${cand.hosts.join(", ")} uses middleware(s) "${carried.join(", ")}" \u2014 not migrated`
804
+ );
805
+ }
806
+ if (cand.portGuessed) {
807
+ warnings.push(
808
+ `traefik: ${cand.hosts.join(", ")} \u2014 no service port label and no single exposed port on ${cand.container}; assumed :80, check the upstream`
809
+ );
810
+ }
811
+ }
812
+ for (const d of dropped) {
813
+ warnings.push(
814
+ `traefik: ${d.hosts.join(", ")} router "${d.router}" is an http\u2192https redirect \u2014 Openship's edge does that itself (nothing to migrate)`
815
+ );
816
+ }
817
+ for (const s of defs.streamRouters) {
818
+ warnings.push(`traefik: ${s} can't migrate \u2014 Openship's edge routes HTTP(S), not raw TCP/UDP`);
819
+ }
820
+ if (defs.fileProvider) {
821
+ warnings.push(
822
+ "traefik: a file provider is configured \u2014 routes defined in its YAML/TOML files are NOT read here, only container labels. Re-add those domains manually."
823
+ );
824
+ }
825
+ return {
826
+ proxy: "traefik",
827
+ sites: sites.map((cand) => ({
828
+ serverNames: cand.hosts,
829
+ ssl: cand.ssl,
830
+ target: { kind: "proxy", url: cand.url },
831
+ source: `traefik container ${cand.container}`
832
+ })),
833
+ warnings
834
+ };
551
835
  }
552
836
  async function scanTraefik(executor) {
553
837
  const out = await tryExec(
554
838
  executor,
555
- "docker ps -q 2>/dev/null | xargs -r docker inspect --format '{{.Name}} {{json .Config.Labels}} {{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}' 2>/dev/null"
839
+ `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`
556
840
  );
557
841
  if (!out) {
558
842
  return { proxy: "traefik", sites: [], warnings: ["traefik: couldn't read container labels via docker inspect"] };
559
843
  }
560
844
  const containers = [];
561
845
  for (const line of out.split("\n")) {
562
- const [rawName, rawLabels, rawIps] = line.split(" ");
846
+ const [rawName, rawLabels, rawNets, rawPorts, rawCmd] = line.split(" ");
563
847
  if (!rawName || !rawLabels) continue;
564
848
  let labels;
565
849
  try {
@@ -567,9 +851,30 @@ async function scanTraefik(executor) {
567
851
  } catch {
568
852
  continue;
569
853
  }
570
- if (!Object.keys(labels).some((k) => k.startsWith("traefik."))) continue;
571
- const ip = (rawIps ?? "").trim().split(/\s+/)[0] || void 0;
572
- containers.push({ name: rawName.replace(/^\//, ""), labels, ip });
854
+ const networks = {};
855
+ let bareIp;
856
+ for (const pair of (rawNets ?? "").trim().split(/\s+/)) {
857
+ if (!pair) continue;
858
+ const eq = pair.indexOf("=");
859
+ if (eq > 0) {
860
+ const ip = pair.slice(eq + 1);
861
+ if (ip) networks[pair.slice(0, eq)] = ip;
862
+ } else if (!bareIp) {
863
+ bareIp = pair;
864
+ }
865
+ }
866
+ const exposedPorts = (rawPorts ?? "").trim().split(/\s+/).filter(Boolean);
867
+ const hasTraefikLabels = Object.keys(labels).some((k) => k.toLowerCase().startsWith("traefik."));
868
+ const looksLikeTraefik = /traefik/i.test(rawCmd ?? "") || /--?providers\./i.test(rawCmd ?? "");
869
+ if (!hasTraefikLabels && !looksLikeTraefik) continue;
870
+ containers.push({
871
+ name: rawName.replace(/^\//, ""),
872
+ labels,
873
+ ip: Object.values(networks)[0] ?? bareIp,
874
+ networks,
875
+ exposedPorts,
876
+ cmd: rawCmd ?? void 0
877
+ });
573
878
  }
574
879
  return parseTraefikLabels(containers);
575
880
  }
@@ -1842,7 +2147,7 @@ async function runningImage(executor, container) {
1842
2147
  return out.trim() || null;
1843
2148
  }
1844
2149
  async function containerEdgeProvider(executor, container, opts) {
1845
- const { NginxProvider: Provider } = await import("./nginx-XWJAOGS4.js");
2150
+ const { NginxProvider: Provider } = await import("./nginx-QE4DOM5J.js");
1846
2151
  return new Provider({
1847
2152
  ...opts,
1848
2153
  // Vhosts go to the BIND-MOUNTED HOST dir (so every host-side reader — the
@@ -1852,21 +2157,38 @@ async function containerEdgeProvider(executor, container, opts) {
1852
2157
  executor: edgeContainerExecutor(executor, container),
1853
2158
  // MUST be pinned: re-detection answers from inside the container and would
1854
2159
  // repoint sitesDir at a host dir the edge never reads.
1855
- pinPaths: true
2160
+ pinPaths: true,
2161
+ // Reload runs INSIDE the container, where the master is pid 1 — so a failed
2162
+ // reload must never fall back to killing it (#292).
2163
+ containerEdge: true
1856
2164
  });
1857
2165
  }
1858
2166
  async function localContainerEdgeProvider(container, opts) {
1859
- const { DockerEdgeExecutor } = await import("./docker-edge-executor-VHW3JPJS.js");
1860
- const { NginxProvider: Provider } = await import("./nginx-XWJAOGS4.js");
2167
+ const { DockerEdgeExecutor } = await import("./docker-edge-executor-5I4M5QT7.js");
2168
+ const { NginxProvider: Provider } = await import("./nginx-QE4DOM5J.js");
1861
2169
  return new Provider({
1862
2170
  ...opts,
1863
2171
  paths: OPENRESTY_DEFAULT_PATHS,
1864
2172
  executor: new DockerEdgeExecutor({ containerName: container }),
1865
2173
  // nginx.conf + the Lua are BAKED into the image — nothing to detect, install
1866
2174
  // or patch, and detection would answer from inside the container anyway.
1867
- pinPaths: true
2175
+ pinPaths: true,
2176
+ // `docker exec`s into the edge, so the master is pid 1 there too (#292).
2177
+ containerEdge: true
1868
2178
  });
1869
2179
  }
2180
+ async function startEdgeContainer(executor, container, image, onLog) {
2181
+ await sanitizeEdgeVhosts(executor, EDGE_HOST_PATHS.sitesDir, onLog).catch(() => {
2182
+ });
2183
+ await executor.exec(`docker rm -f ${sq(container)} 2>/dev/null || true`).catch(() => {
2184
+ });
2185
+ const run = await executor.streamExec(
2186
+ buildEdgeRunCommand(container, image),
2187
+ onLog
2188
+ );
2189
+ invalidateEdgeContainer(executor);
2190
+ return run.code === 0;
2191
+ }
1870
2192
  function buildEdgeRunCommand(container, image) {
1871
2193
  const mounts = EDGE_CONTAINER_MOUNTS.map(
1872
2194
  // `:z` relabels for SELinux-enforcing hosts; a no-op elsewhere.
@@ -1890,14 +2212,7 @@ async function swapEdgeImage(executor, container, from, to, opts) {
1890
2212
  return { swapped: false, edgeDown: false };
1891
2213
  }
1892
2214
  const start = async (image) => {
1893
- await executor.exec(`docker rm -f ${sq(container)} 2>/dev/null || true`).catch(() => {
1894
- });
1895
- const run = await executor.streamExec(
1896
- buildEdgeRunCommand(container, image),
1897
- onLog
1898
- );
1899
- invalidateEdgeContainer(executor);
1900
- if (run.code !== 0) return false;
2215
+ if (!await startEdgeContainer(executor, container, image, onLog)) return false;
1901
2216
  const listening = await waitForPortListening(executor, 80, {
1902
2217
  timeoutMs: opts.verifyTimeoutMs ?? 3e4
1903
2218
  });
@@ -1920,29 +2235,6 @@ async function listDir(executor, dir) {
1920
2235
  out.split("\n").map((l) => l.trim()).filter(Boolean)
1921
2236
  );
1922
2237
  }
1923
- async function sanitizeCarriedVhosts(executor, sitesDir, onLog) {
1924
- const script = [
1925
- `for f in ${sq(sitesDir)}/*.conf; do`,
1926
- ` [ -f "$f" ] || continue;`,
1927
- // A real server_name is anything that isn't the `_` wildcard.
1928
- ` if ! grep -qE '^[[:space:]]*server_name[[:space:]]+[^_;[:space:]]' "$f"; then`,
1929
- ` echo "dropped-catchall $f"; rm -f "$f"; continue;`,
1930
- ` fi;`,
1931
- ` if grep -qE '[[:space:]]default_server' "$f"; then`,
1932
- ` sed -i -E 's/([[:space:]]listen[^;]*)[[:space:]]+default_server/\\1/g' "$f" && echo "unset-default $f";`,
1933
- ` fi;`,
1934
- `done`
1935
- ].join(" ");
1936
- const out = await executor.exec(script).catch(() => "");
1937
- for (const line of out.split("\n").map((l) => l.trim()).filter(Boolean)) {
1938
- const [action, file] = [line.slice(0, line.indexOf(" ")), line.slice(line.indexOf(" ") + 1)];
1939
- if (action === "dropped-catchall") {
1940
- onLog(log4(`Dropped the bare edge's catch-all ${file} \u2014 the edge image provides it.`, "warn"));
1941
- } else if (action === "unset-default") {
1942
- onLog(log4(`Removed default_server from ${file} \u2014 the edge image owns it.`, "warn"));
1943
- }
1944
- }
1945
- }
1946
2238
  async function ensureContainerEdge(executor, opts) {
1947
2239
  const { onLog } = opts;
1948
2240
  const container = opts.container?.trim() || EDGE_CONTAINER_NAME;
@@ -1989,7 +2281,6 @@ async function ensureContainerEdge(executor, opts) {
1989
2281
  beforeCarry = await listDir(executor, sitesTarget);
1990
2282
  await executor.exec(`cp -a ${sq(`${barePaths.sitesDir}/.`)} ${sq(`${sitesTarget}/`)} 2>/dev/null || true`).catch(() => {
1991
2283
  });
1992
- await sanitizeCarriedVhosts(executor, sitesTarget, onLog);
1993
2284
  }
1994
2285
  }
1995
2286
  const restoreBare = async () => {
@@ -2015,15 +2306,10 @@ async function ensureContainerEdge(executor, opts) {
2015
2306
  await executor.exec("systemctl reset-failed openresty 2>/dev/null || true").catch(() => {
2016
2307
  });
2017
2308
  }
2018
- await executor.exec(`docker rm -f ${sq(container)} 2>/dev/null || true`).catch(() => {
2019
- });
2020
2309
  onLog(log4("Starting the edge container..."));
2021
- const run = await executor.streamExec(
2022
- buildEdgeRunCommand(container, image),
2023
- onLog
2024
- );
2025
- invalidateEdgeContainer(executor);
2026
- if (run.code !== 0) throw new Error("the edge container failed to start");
2310
+ if (!await startEdgeContainer(executor, container, image, onLog)) {
2311
+ throw new Error("the edge container failed to start");
2312
+ }
2027
2313
  await executor.exec(containerCommand(container, "openresty -t"));
2028
2314
  const listening = await waitForPortListening(executor, 80, {
2029
2315
  timeoutMs: opts.verifyTimeoutMs ?? 3e4