openship 0.4.8 → 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-JY5Y5ELU.js → bare-VRGE5MXH.js} +7 -7
  3. package/dist/{chunk-TGIINPVM.js → chunk-6LKHXB6T.js} +22 -7
  4. package/dist/{chunk-2D3I5G6P.js → chunk-6PIYRG5I.js} +23 -3
  5. package/dist/{chunk-BBHX7T4R.js → chunk-BS2AXYZX.js} +401 -76
  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-5NYUCONN.js → chunk-JLKKYUZA.js} +31 -49
  10. package/dist/{chunk-XPPZ7B2J.js → chunk-K2RPVGPN.js} +2 -2
  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-GKHUSZCD.js → chunk-NNPEG6HM.js} +1 -1
  14. package/dist/{chunk-SLTYVHU7.js → chunk-NXFIRK2Z.js} +381 -72
  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-KYWY3RP4.js → cloud-3M2WZ624.js} +12 -12
  21. package/dist/{detect-AZLOHUFX.js → detect-BAJMY6KM.js} +3 -3
  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-ABUDKKJI.js → edge-import-RIWNEIXQ.js} +21 -19
  25. package/dist/{ensure-container-edge-SW6HENSG.js → ensure-container-edge-3NSYKHRX.js} +6 -6
  26. package/dist/{executor-XJ6RP4Q2.js → executor-LOYMPU67.js} +3 -3
  27. package/dist/index.js +269 -331
  28. package/dist/{nginx-3L4SIBJA.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 +51643 -45322
  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-NU6T2PDU.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";
@@ -34,7 +34,7 @@ import {
34
34
  sanitizeEdgeVhosts,
35
35
  sq as sq2,
36
36
  stopTargetsForStatus
37
- } from "./chunk-2D3I5G6P.js";
37
+ } from "./chunk-6PIYRG5I.js";
38
38
  import {
39
39
  waitForPortListening
40
40
  } from "./chunk-ILFETZOM.js";
@@ -43,12 +43,12 @@ import {
43
43
  EDGE_HOST_PATHS,
44
44
  OPENRESTY_DEFAULT_PATHS,
45
45
  detectOpenRestyPaths
46
- } from "./chunk-VFINNTUL.js";
46
+ } from "./chunk-EAAGSQXN.js";
47
47
  import {
48
48
  buildEdgeImageRef,
49
49
  compareSemver,
50
50
  safeErrorMessage
51
- } from "./chunk-6APULWSU.js";
51
+ } from "./chunk-UMQP7SYT.js";
52
52
 
53
53
  // ../../packages/adapters/src/system/proxy/import/parse-utils.ts
54
54
  async function tryExec(executor, command) {
@@ -82,6 +82,24 @@ function extractBlocks(text, keyword) {
82
82
  }
83
83
  return blocks;
84
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
+ }
85
103
  function sq3(value) {
86
104
  return `'${value.replace(/'/g, "'\\''")}'`;
87
105
  }
@@ -122,7 +140,10 @@ function resolveProxyTarget(proxyPass, upstreams) {
122
140
  const host = authority.replace(/:\d+$/, "");
123
141
  if (upstreams.has(host)) return { url: `${scheme}${upstreams.get(host)}` };
124
142
  const isIp = /^\d{1,3}(\.\d{1,3}){3}$/.test(host);
125
- 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 };
126
147
  return { reason: `proxy_pass host "${host}" is an undeclared upstream \u2014 not migratable` };
127
148
  }
128
149
  function extractLocationProxies(serverBody) {
@@ -160,6 +181,16 @@ function isHttpsUpgradeForSelf(body, names) {
160
181
  return names.includes(host);
161
182
  });
162
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
+ }
163
194
  function parseServer(body, source, upstreams) {
164
195
  const warnings = [];
165
196
  const names = firstDirective(body, "server_name")?.split(/\s+/).filter((n) => n && n !== "_" && !n.startsWith("~")) ?? [];
@@ -186,10 +217,12 @@ function parseServer(body, source, upstreams) {
186
217
  const primary = resolved.find((r) => r.path === "/") ?? resolved[0];
187
218
  target = { kind: "proxy", url: primary.url };
188
219
  routes = resolved;
189
- } else if (root) {
190
- target = { kind: "static", root: root.replace(/;$/, "") };
191
220
  } else if (isHttpsUpgradeForSelf(body, names)) {
192
221
  return { warnings: [] };
222
+ } else if (root && !isAcmeWebrootOnly(body)) {
223
+ target = { kind: "static", root: root.replace(/;$/, "") };
224
+ } else if (root) {
225
+ return { warnings: [] };
193
226
  } else {
194
227
  warnings.push(`nginx: ${names[0]} has neither proxy_pass nor root \u2014 skipped (${source})`);
195
228
  return { warnings };
@@ -216,7 +249,12 @@ function parseNginxConfig(raw) {
216
249
  warnings.push(...blockWarnings);
217
250
  if (site) sites.push(site);
218
251
  }
219
- 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 };
220
258
  }
221
259
  async function scanNginx(executor) {
222
260
  return parseNginxConfig(await loadNginxConfig(executor));
@@ -245,23 +283,61 @@ async function loadCaddyJson(executor) {
245
283
  }
246
284
  return null;
247
285
  }
248
- function flattenHandlers(handlers, out = []) {
286
+ function flattenHandlers(handlers, scoped = false, out = []) {
249
287
  for (const h of handlers ?? []) {
250
288
  if (h.handler === "subroute") {
251
- 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
+ }
252
292
  } else {
253
- out.push(h);
293
+ out.push({ h, scoped });
254
294
  }
255
295
  }
256
296
  return out;
257
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
+ };
258
309
  function targetFromRoute(route) {
259
310
  const flat = flattenHandlers(route.handle);
260
- const dial = flat.find((h) => h.handler === "reverse_proxy")?.upstreams?.find((u) => u.dial)?.dial;
261
- if (dial) return { kind: "proxy", url: /^https?:\/\//.test(dial) ? dial : `http://${dial}` };
262
- const root = flat.find((h) => h.handler === "vars" && typeof h.root === "string")?.root;
263
- if (root && flat.some((h) => h.handler === "file_server")) return { kind: "static", root };
264
- 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 {};
265
341
  }
266
342
  function parseCaddyJson(raw) {
267
343
  let cfg;
@@ -273,17 +349,47 @@ function parseCaddyJson(raw) {
273
349
  const servers = cfg.apps?.http?.servers;
274
350
  if (!servers) return null;
275
351
  const sites = [];
352
+ const warnings = [];
276
353
  for (const srv of Object.values(servers)) {
277
354
  const ssl = (srv.listen ?? []).some((l) => l.endsWith(":443"));
278
355
  for (const route of srv.routes ?? []) {
279
356
  const hosts = (route.match ?? []).flatMap((m) => m.host ?? []).filter((h) => h && h !== "*");
280
357
  if (hosts.length === 0) continue;
281
- const target = targetFromRoute(route);
282
- if (!target) continue;
283
- 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)" });
284
380
  }
285
381
  }
286
- 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 };
287
393
  }
288
394
  async function loadCaddyfile(executor) {
289
395
  for (const p of CADDYFILE_PATHS) {
@@ -327,7 +433,7 @@ async function scanCaddy(executor) {
327
433
  const json = await loadCaddyJson(executor);
328
434
  if (json) {
329
435
  const parsed = parseCaddyJson(json);
330
- if (parsed && parsed.sites.length > 0) {
436
+ if (parsed && (parsed.sites.length > 0 || parsed.warnings.length > 0)) {
331
437
  return { proxy: "caddy", sites: parsed.sites, warnings: parsed.warnings };
332
438
  }
333
439
  }
@@ -357,19 +463,44 @@ async function scanCaddyText(executor) {
357
463
  warnings.push(`caddy: skipped a block with no usable address (${header.slice(0, 40)})`);
358
464
  continue;
359
465
  }
360
- 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]+)/);
361
469
  const rootMatch = body.match(/(?:^|\s)root\s+\*?\s*([^\n\s]+)/);
362
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;
363
473
  let target;
364
- if (proxyMatch) {
365
- 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
+ }
366
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;
367
493
  } else if (rootMatch) {
368
494
  target = { kind: "static", root: rootMatch[1] };
369
495
  } else {
370
- 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`);
371
497
  continue;
372
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
+ }
373
504
  const site = {
374
505
  serverNames: addrs.map((a) => a.host),
375
506
  ssl: addrs.some((a) => a.ssl),
@@ -381,7 +512,17 @@ async function scanCaddyText(executor) {
381
512
  }
382
513
  sites.push(site);
383
514
  }
384
- 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 };
385
526
  }
386
527
 
387
528
  // ../../packages/adapters/src/system/proxy/import/apache.ts
@@ -419,24 +560,51 @@ function directiveAll(body, name) {
419
560
  (m) => m[1].trim()
420
561
  );
421
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
+ }
422
575
  function parseVhost(body, portHint) {
423
576
  const serverName = directive(body, "ServerName")?.split(/\s+/)[0];
424
577
  const aliases = directiveAll(body, "ServerAlias").flatMap((line) => line.split(/\s+/));
425
- const names = [serverName, ...aliases].filter((n) => Boolean(n));
578
+ const names = [serverName, ...aliases].filter((n) => Boolean(n)).map(hostOnly).filter(Boolean);
426
579
  if (names.length === 0) {
427
580
  return { warning: "apache: skipped a VirtualHost with no ServerName" };
428
581
  }
582
+ if (isHttpsRedirectOnly(body)) return null;
429
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 !== "!");
430
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);
431
586
  const certPath = directive(body, "SSLCertificateFile");
432
587
  const keyPath = directive(body, "SSLCertificateKeyFile");
433
588
  const ssl = /:443/.test(portHint) || /SSLEngine\s+on/i.test(body) || Boolean(certPath);
434
589
  let target;
435
590
  if (routes.length > 0) {
436
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
+ }
437
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
+ };
438
602
  } else if (docRoot) {
439
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
+ };
440
608
  } else {
441
609
  return { warning: `apache: ${names[0]} has neither ProxyPass nor DocumentRoot \u2014 skipped` };
442
610
  }
@@ -445,6 +613,11 @@ function parseVhost(body, portHint) {
445
613
  if (certPath && keyPath) site.tls = { certPath, keyPath };
446
614
  return site;
447
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
+ }
448
621
  function vhostBlocks(text) {
449
622
  const out = [];
450
623
  const re = /<VirtualHost([^>]*)>([\s\S]*?)<\/VirtualHost>/gi;
@@ -464,15 +637,44 @@ async function scanApache(executor) {
464
637
  const text = stripComments(raw);
465
638
  const blocks = vhostBlocks(text);
466
639
  if (blocks.length === 0) warnings.push("apache: no <VirtualHost> blocks found");
640
+ const redirectOnlyHosts = [];
467
641
  for (const { args, body } of blocks) {
468
642
  const parsed = parseVhost(body, args);
643
+ if (parsed === null) {
644
+ redirectOnlyHosts.push(redirectVhostNames(body));
645
+ continue;
646
+ }
469
647
  if ("warning" in parsed) warnings.push(parsed.warning);
470
648
  else sites.push(parsed);
471
649
  }
472
- 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 };
473
670
  }
474
671
 
475
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
+ }
476
678
  function extractHosts(rule) {
477
679
  const hosts = [];
478
680
  for (const call of rule.matchAll(/\bHost\(([^)]*)\)/gi)) {
@@ -481,7 +683,7 @@ function extractHosts(rule) {
481
683
  if (h) hosts.push(h);
482
684
  }
483
685
  }
484
- return hosts;
686
+ return [...new Set(hosts)];
485
687
  }
486
688
  function extraMatchers(rule) {
487
689
  const found = /* @__PURE__ */ new Set();
@@ -492,23 +694,59 @@ function extraMatchers(rule) {
492
694
  }
493
695
  return [...found];
494
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
+ }
495
735
  function parseTraefikLabels(containers) {
496
- const sites = [];
497
736
  const warnings = [];
737
+ const defs = collectDefinitions(containers);
738
+ const candidates = [];
498
739
  for (const c of containers) {
499
- 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;
500
743
  const routers = /* @__PURE__ */ new Map();
501
- const servicePorts = /* @__PURE__ */ new Map();
502
- for (const [key, val] of Object.entries(c.labels)) {
744
+ for (const [key, val] of Object.entries(labels)) {
503
745
  const r = key.match(/^traefik\.http\.routers\.([^.]+)\.(.+)$/);
504
- if (r) {
505
- const bag = routers.get(r[1]) ?? {};
506
- bag[r[2]] = val;
507
- routers.set(r[1], bag);
508
- continue;
509
- }
510
- const s = key.match(/^traefik\.http\.services\.([^.]+)\.loadbalancer\.server\.port$/);
511
- 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);
512
750
  }
513
751
  for (const [rname, props] of routers) {
514
752
  const rule = props.rule;
@@ -520,47 +758,92 @@ function parseTraefikLabels(containers) {
520
758
  );
521
759
  continue;
522
760
  }
523
- if (!c.ip) {
761
+ const ip = resolveIp(c, labels);
762
+ if (!ip) {
524
763
  warnings.push(
525
764
  `traefik: ${hosts.join(", ")} \u2014 couldn't resolve container ${c.name}'s IP; re-add the upstream manually`
526
765
  );
527
766
  continue;
528
767
  }
529
- const svc = props.service;
530
- 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;
531
772
  const port = /^\d{1,5}$/.test(String(rawPort)) ? String(rawPort) : "80";
532
- const url = `http://${c.ip}:${port}`;
533
- const extras = extraMatchers(rule);
534
- if (extras.length > 0) {
535
- warnings.push(
536
- `traefik: ${hosts.join(", ")} also matches ${extras.join(", ")} \u2014 only the Host() part is migrated; re-add path/header rules manually`
537
- );
538
- }
539
- if (props.middlewares) {
540
- warnings.push(`traefik: ${hosts.join(", ")} uses middleware(s) "${props.middlewares}" \u2014 not migrated`);
541
- }
542
- const ssl = Object.keys(props).some((p) => p === "tls" || p.startsWith("tls."));
543
- sites.push({
544
- serverNames: hosts,
545
- ssl,
546
- target: { kind: "proxy", url },
547
- 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
548
785
  });
549
786
  }
550
787
  }
551
- 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
+ };
552
835
  }
553
836
  async function scanTraefik(executor) {
554
837
  const out = await tryExec(
555
838
  executor,
556
- "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`
557
840
  );
558
841
  if (!out) {
559
842
  return { proxy: "traefik", sites: [], warnings: ["traefik: couldn't read container labels via docker inspect"] };
560
843
  }
561
844
  const containers = [];
562
845
  for (const line of out.split("\n")) {
563
- const [rawName, rawLabels, rawIps] = line.split(" ");
846
+ const [rawName, rawLabels, rawNets, rawPorts, rawCmd] = line.split(" ");
564
847
  if (!rawName || !rawLabels) continue;
565
848
  let labels;
566
849
  try {
@@ -568,9 +851,30 @@ async function scanTraefik(executor) {
568
851
  } catch {
569
852
  continue;
570
853
  }
571
- if (!Object.keys(labels).some((k) => k.startsWith("traefik."))) continue;
572
- const ip = (rawIps ?? "").trim().split(/\s+/)[0] || void 0;
573
- 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
+ });
574
878
  }
575
879
  return parseTraefikLabels(containers);
576
880
  }
@@ -1843,7 +2147,7 @@ async function runningImage(executor, container) {
1843
2147
  return out.trim() || null;
1844
2148
  }
1845
2149
  async function containerEdgeProvider(executor, container, opts) {
1846
- const { NginxProvider: Provider } = await import("./nginx-3L4SIBJA.js");
2150
+ const { NginxProvider: Provider } = await import("./nginx-QE4DOM5J.js");
1847
2151
  return new Provider({
1848
2152
  ...opts,
1849
2153
  // Vhosts go to the BIND-MOUNTED HOST dir (so every host-side reader — the
@@ -1853,19 +2157,24 @@ async function containerEdgeProvider(executor, container, opts) {
1853
2157
  executor: edgeContainerExecutor(executor, container),
1854
2158
  // MUST be pinned: re-detection answers from inside the container and would
1855
2159
  // repoint sitesDir at a host dir the edge never reads.
1856
- 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
1857
2164
  });
1858
2165
  }
1859
2166
  async function localContainerEdgeProvider(container, opts) {
1860
- const { DockerEdgeExecutor } = await import("./docker-edge-executor-VHW3JPJS.js");
1861
- const { NginxProvider: Provider } = await import("./nginx-3L4SIBJA.js");
2167
+ const { DockerEdgeExecutor } = await import("./docker-edge-executor-5I4M5QT7.js");
2168
+ const { NginxProvider: Provider } = await import("./nginx-QE4DOM5J.js");
1862
2169
  return new Provider({
1863
2170
  ...opts,
1864
2171
  paths: OPENRESTY_DEFAULT_PATHS,
1865
2172
  executor: new DockerEdgeExecutor({ containerName: container }),
1866
2173
  // nginx.conf + the Lua are BAKED into the image — nothing to detect, install
1867
2174
  // or patch, and detection would answer from inside the container anyway.
1868
- pinPaths: true
2175
+ pinPaths: true,
2176
+ // `docker exec`s into the edge, so the master is pid 1 there too (#292).
2177
+ containerEdge: true
1869
2178
  });
1870
2179
  }
1871
2180
  async function startEdgeContainer(executor, container, image, onLog) {