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.
- package/README.md +1 -0
- package/dist/{bare-JY5Y5ELU.js → bare-VRGE5MXH.js} +7 -7
- package/dist/{chunk-TGIINPVM.js → chunk-6LKHXB6T.js} +22 -7
- package/dist/{chunk-2D3I5G6P.js → chunk-6PIYRG5I.js} +23 -3
- package/dist/{chunk-BBHX7T4R.js → chunk-BS2AXYZX.js} +401 -76
- package/dist/{chunk-SJIFLLX4.js → chunk-CUJXGK7R.js} +321 -85
- package/dist/{chunk-VFINNTUL.js → chunk-EAAGSQXN.js} +23 -8
- package/dist/{chunk-4Z5QO7N6.js → chunk-I6PSTQIR.js} +2 -2
- package/dist/{chunk-5NYUCONN.js → chunk-JLKKYUZA.js} +31 -49
- package/dist/{chunk-XPPZ7B2J.js → chunk-K2RPVGPN.js} +2 -2
- package/dist/{chunk-MZ2WEHHY.js → chunk-KNTJ7PSF.js} +1 -1
- package/dist/{chunk-L2VOGB2X.js → chunk-KZOBYJUW.js} +1 -1
- package/dist/{chunk-GKHUSZCD.js → chunk-NNPEG6HM.js} +1 -1
- package/dist/{chunk-SLTYVHU7.js → chunk-NXFIRK2Z.js} +381 -72
- package/dist/{chunk-FXTBHTWY.js → chunk-RB35YN3J.js} +271 -35
- package/dist/{chunk-6APULWSU.js → chunk-UMQP7SYT.js} +429 -66
- package/dist/{chunk-V6AN6XLZ.js → chunk-V7BZMGOT.js} +123 -45
- package/dist/{chunk-VDMQJHPI.js → chunk-YWMODKWL.js} +67 -4
- package/dist/{chunk-OXWDY22G.js → chunk-ZCOQJCZO.js} +1 -1
- package/dist/{cloud-KYWY3RP4.js → cloud-3M2WZ624.js} +12 -12
- package/dist/{detect-AZLOHUFX.js → detect-BAJMY6KM.js} +3 -3
- package/dist/{docker-LODK5Y2S.js → docker-2NGBRJFJ.js} +9 -7
- package/dist/{docker-edge-executor-VHW3JPJS.js → docker-edge-executor-5I4M5QT7.js} +1 -1
- package/dist/{edge-import-ABUDKKJI.js → edge-import-RIWNEIXQ.js} +21 -19
- package/dist/{ensure-container-edge-SW6HENSG.js → ensure-container-edge-3NSYKHRX.js} +6 -6
- package/dist/{executor-XJ6RP4Q2.js → executor-LOYMPU67.js} +3 -3
- package/dist/index.js +269 -331
- package/dist/{nginx-3L4SIBJA.js → nginx-QE4DOM5J.js} +4 -4
- package/dist/{openresty-lua-4WLGDSER.js → openresty-lua-BTAKECCF.js} +2 -2
- package/dist/server/index.js +51643 -45322
- package/dist/server/migrations/0073_project_volumes_object_storage.sql +3 -0
- package/dist/server/migrations/0074_project_compose_path.sql +1 -0
- package/dist/server/migrations/0075_service_command_argv.sql +1 -0
- package/dist/server/migrations/0076_rollback_retention.sql +17 -0
- package/dist/server/migrations/0077_domain_redirect.sql +13 -0
- package/dist/server/migrations/0078_grant_source_scope.sql +30 -0
- package/dist/server/migrations/0079_project_readiness.sql +18 -0
- package/dist/server/migrations/0080_deployment_error_classification.sql +26 -0
- package/dist/server/migrations/0081_project_readiness_repair.sql +34 -0
- package/dist/server/migrations/meta/_journal.json +63 -0
- package/dist/{setup-NU6T2PDU.js → setup-KELALK7H.js} +7 -7
- 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-
|
|
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-
|
|
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-
|
|
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-
|
|
46
|
+
} from "./chunk-EAAGSQXN.js";
|
|
47
47
|
import {
|
|
48
48
|
buildEdgeImageRef,
|
|
49
49
|
compareSemver,
|
|
50
50
|
safeErrorMessage
|
|
51
|
-
} from "./chunk-
|
|
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 (
|
|
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
|
-
|
|
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 ?? [])
|
|
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
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
if (
|
|
264
|
-
|
|
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
|
|
282
|
-
if (
|
|
283
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
365
|
-
const upstream =
|
|
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: ${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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-
|
|
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-
|
|
1861
|
-
const { NginxProvider: Provider } = await import("./nginx-
|
|
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) {
|