openship 0.4.8 → 0.6.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 +27 -1
- package/dist/bare-FSUAM5V5.js +23 -0
- package/dist/{bowser-O3AEA6AN.js → bowser-HNOTPGJO.js} +2 -2
- package/dist/{chunk-5MZ624VX.js → chunk-2GWYYHOR.js} +1 -1
- package/dist/chunk-3FYIXVX5.js +8051 -0
- package/dist/{chunk-ILFETZOM.js → chunk-3LDWWY7U.js} +21 -1
- package/dist/{chunk-W73X43R7.js → chunk-3NQQXMDR.js} +3 -3
- package/dist/{chunk-OWED2GRM.js → chunk-42WQABXM.js} +2 -2
- package/dist/{chunk-TYVME3XD.js → chunk-6FH4AXVY.js} +20 -3
- package/dist/{chunk-OXWDY22G.js → chunk-6ODUBKUB.js} +3 -3
- package/dist/{chunk-DYPH4HCI.js → chunk-AO252K7Z.js} +1 -1
- package/dist/{chunk-XPPZ7B2J.js → chunk-B3CE2OMR.js} +14 -12
- package/dist/{chunk-IXCHURVK.js → chunk-DOD2BXHQ.js} +4 -4
- package/dist/{chunk-VWPEKOH6.js → chunk-DRK3MEDS.js} +3 -3
- package/dist/{chunk-V6AN6XLZ.js → chunk-EMXI4UCU.js} +125 -47
- package/dist/{chunk-GKHUSZCD.js → chunk-EP2TNALB.js} +2 -2
- package/dist/{chunk-SLTYVHU7.js → chunk-GUQ3QCJ7.js} +942 -519
- package/dist/chunk-I3DPMTSF.js +29 -0
- package/dist/{chunk-L2VOGB2X.js → chunk-IW2UKBVF.js} +3 -3
- package/dist/{chunk-VDMQJHPI.js → chunk-IXM5245F.js} +228 -13
- package/dist/{chunk-MZ2WEHHY.js → chunk-JIMUYEKN.js} +4 -4
- package/dist/chunk-OGY63DGD.js +705 -0
- package/dist/{chunk-O6ZZF52V.js → chunk-ONKEYSMY.js} +2 -2
- package/dist/{chunk-6APULWSU.js → chunk-OTLSJYAC.js} +1744 -118
- package/dist/{chunk-EI575GHN.js → chunk-PDW4L3NV.js} +3 -3
- package/dist/{chunk-5NYUCONN.js → chunk-PSZSKEPA.js} +33 -51
- package/dist/{chunk-KI2EY3WB.js → chunk-PXKCZB2I.js} +1 -1
- package/dist/{chunk-FXTBHTWY.js → chunk-QEH4QUBX.js} +649 -85
- package/dist/{chunk-6APIPIJY.js → chunk-QWHBRY2K.js} +5 -5
- package/dist/{chunk-6B5YCFFV.js → chunk-RVER2EOO.js} +5 -5
- package/dist/{chunk-Q2CIRAOK.js → chunk-RXQBD4ZY.js} +1 -1
- package/dist/{chunk-TGIINPVM.js → chunk-TWJCLD4W.js} +60 -10
- package/dist/{chunk-4Z5QO7N6.js → chunk-VDDMADOF.js} +3 -3
- package/dist/{chunk-BBHX7T4R.js → chunk-VE2YOK4I.js} +991 -254
- package/dist/{chunk-2D3I5G6P.js → chunk-VZ2ZHPS2.js} +73 -43
- package/dist/{chunk-AVIB7CT7.js → chunk-WNWQBB7F.js} +2 -2
- package/dist/{chunk-FRNAR33O.js → chunk-XZYFZ6A5.js} +3 -3
- package/dist/{chunk-RPHRPFEH.js → chunk-ZCFVHA25.js} +1 -1
- package/dist/{chunk-FPRHYBY2.js → chunk-ZFJXPCQP.js} +1 -1
- package/dist/{chunk-JYGD5WSN.js → chunk-ZWLDLTJP.js} +1 -1
- package/dist/cloud-E5M6VMCG.js +31 -0
- package/dist/{cloud-7QYOGF7F.js → cloud-KRTZ2IL5.js} +3 -3
- package/dist/{detect-AZLOHUFX.js → detect-ALF4GXRX.js} +7 -8
- package/dist/{dist-IQJ6H6UZ.js → dist-BM6NPWIH.js} +3 -3
- package/dist/{dist-es-X3B5QYKB.js → dist-es-2YRDXMUU.js} +6 -6
- package/dist/{dist-es-OSNJBLXB.js → dist-es-CCVZEFRZ.js} +5 -5
- package/dist/{dist-es-VNKHQVIB.js → dist-es-IOH6G4GM.js} +4 -4
- package/dist/{dist-es-RYHIPOTN.js → dist-es-JGBPABJI.js} +14 -14
- package/dist/{dist-es-JSTDUS7Q.js → dist-es-KWSMDPIP.js} +6 -6
- package/dist/{dist-es-5DY7K32L.js → dist-es-UYDDOVZ3.js} +6 -6
- package/dist/{dist-es-WV4AMOII.js → dist-es-XUOR4NQN.js} +8 -8
- package/dist/docker-QECQJ5P4.js +32 -0
- package/dist/docker-edge-executor-62N64MGS.js +17 -0
- package/dist/edge-import-CQWHN2Q3.js +48 -0
- package/dist/ensure-container-edge-MJVXJQG5.js +32 -0
- package/dist/{event-streams-EHRAKL75.js → event-streams-TR4GAC53.js} +4 -4
- package/dist/{executor-XJ6RP4Q2.js → executor-JELARVKC.js} +8 -8
- package/dist/index.js +1209 -560
- package/dist/{loadSso-46FQRIZS.js → loadSso-UGF4JMRK.js} +10 -10
- package/dist/{local-executor-V7U5VOZW.js → local-executor-WOFZD72X.js} +4 -4
- package/dist/nginx-AXGNWRKZ.js +31 -0
- package/dist/node-entry.js +4 -0
- package/dist/{noop-FMMFG2EZ.js → noop-NJ72F46T.js} +3 -3
- package/dist/openresty-lua-IE4P5YQX.js +81 -0
- package/dist/server/index.js +116248 -94749
- package/dist/server/lua/geo_country.lua +31 -6
- package/dist/server/lua/maxminddb.lua +403 -0
- package/dist/server/lua/mgmt_api.lua +431 -68
- package/dist/server/lua/pipe_log.lua +10 -4
- package/dist/server/lua/site_logger.lua +220 -12
- 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/0082_edge_target_verification.sql +39 -0
- package/dist/server/migrations/0083_service_incident.sql +84 -0
- package/dist/server/migrations/0084_analytics_daily_rollup.sql +72 -0
- package/dist/server/migrations/0085_resource_usage.sql +68 -0
- package/dist/server/migrations/0086_remote_infra_updates.sql +41 -0
- package/dist/server/migrations/0087_server_container_version_columns.sql +17 -0
- package/dist/server/migrations/0088_audit_source_and_settings.sql +27 -0
- package/dist/server/migrations/0089_auto_scan_infra.sql +7 -0
- package/dist/server/migrations/0090_project_collect_paths.sql +18 -0
- package/dist/server/migrations/0091_project_server_id.sql +26 -0
- package/dist/server/migrations/0092_project_internal_alias.sql +13 -0
- package/dist/server/migrations/0093_service_incident_org_index.sql +13 -0
- package/dist/server/migrations/0094_backup_restore_meta.sql +15 -0
- package/dist/server/migrations/0095_backup_restore_cancel.sql +19 -0
- package/dist/server/migrations/0096_backup_policy_retention_defaults.sql +20 -0
- package/dist/server/migrations/0097_update_status_upstream_only.sql +35 -0
- package/dist/server/migrations/meta/_journal.json +175 -0
- package/dist/setup-FA4PW4NU.js +21 -0
- package/dist/{signin-ITOPKKUB.js → signin-3M3UPB6K.js} +10 -10
- package/dist/{sso-oidc-NON2PD7Y.js → sso-oidc-3VDIYOEJ.js} +10 -10
- package/dist/{sts-HHFFSAY2.js → sts-B3WIYLXR.js} +9 -9
- package/package.json +6 -3
- package/dist/bare-JY5Y5ELU.js +0 -23
- package/dist/chunk-SJIFLLX4.js +0 -2931
- package/dist/chunk-VFINNTUL.js +0 -362
- package/dist/cloud-KYWY3RP4.js +0 -30
- package/dist/docker-LODK5Y2S.js +0 -16
- package/dist/docker-edge-executor-VHW3JPJS.js +0 -13
- package/dist/edge-import-ABUDKKJI.js +0 -45
- package/dist/ensure-container-edge-SW6HENSG.js +0 -31
- package/dist/nginx-3L4SIBJA.js +0 -24
- package/dist/openresty-lua-4WLGDSER.js +0 -42
- package/dist/setup-NU6T2PDU.js +0 -20
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env sh
|
|
2
|
-
":" //# ; exec "$(command -v
|
|
2
|
+
":" //# ; exec "$(command -v node || command -v bun)" "$0" "$@"
|
|
3
3
|
import { createRequire as __ospCreateRequire } from "node:module";
|
|
4
4
|
const require = __ospCreateRequire(import.meta.url);
|
|
5
5
|
import {
|
|
6
6
|
resolveEnvironment
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-IW2UKBVF.js";
|
|
8
8
|
import {
|
|
9
9
|
formatDuration,
|
|
10
10
|
isRemoteConnectionError,
|
|
11
11
|
systemDebug
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-ZCFVHA25.js";
|
|
13
13
|
import {
|
|
14
14
|
containerCommand,
|
|
15
15
|
edgeContainerExecutor,
|
|
@@ -17,41 +17,250 @@ import {
|
|
|
17
17
|
readDeclaredPair,
|
|
18
18
|
readMaybeInContainer,
|
|
19
19
|
validateCertFor
|
|
20
|
-
} from "./chunk-
|
|
21
|
-
import {
|
|
22
|
-
sq
|
|
23
|
-
} from "./chunk-FPRHYBY2.js";
|
|
20
|
+
} from "./chunk-QEH4QUBX.js";
|
|
24
21
|
import {
|
|
25
22
|
EDGE_CONTAINER_NAME,
|
|
26
23
|
EdgeConflictError,
|
|
27
24
|
EdgeMigrateRequested,
|
|
25
|
+
edgeCrashReason,
|
|
28
26
|
edgeFailureReason,
|
|
27
|
+
edgeIsBroken,
|
|
29
28
|
freeEdgeTargets,
|
|
30
29
|
invalidateEdgeContainer,
|
|
31
|
-
ourLuaOnHost,
|
|
32
30
|
probeEdge,
|
|
33
31
|
resolveOurEdgeContainer,
|
|
34
32
|
sanitizeEdgeVhosts,
|
|
35
33
|
sq as sq2,
|
|
36
34
|
stopTargetsForStatus
|
|
37
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-VZ2ZHPS2.js";
|
|
38
36
|
import {
|
|
39
37
|
waitForPortListening
|
|
40
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-3LDWWY7U.js";
|
|
41
39
|
import {
|
|
40
|
+
EDGE_CHALLENGE_HOST_DIR,
|
|
42
41
|
EDGE_CONTAINER_MOUNTS,
|
|
43
42
|
EDGE_HOST_PATHS,
|
|
44
|
-
OPENRESTY_DEFAULT_PATHS
|
|
45
|
-
|
|
46
|
-
} from "./chunk-VFINNTUL.js";
|
|
43
|
+
OPENRESTY_DEFAULT_PATHS
|
|
44
|
+
} from "./chunk-OGY63DGD.js";
|
|
47
45
|
import {
|
|
46
|
+
sq
|
|
47
|
+
} from "./chunk-ZFJXPCQP.js";
|
|
48
|
+
import {
|
|
49
|
+
AppError,
|
|
50
|
+
PROXY_DIRECTIVES,
|
|
48
51
|
buildEdgeImageRef,
|
|
49
52
|
compareSemver,
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
parseProxyValue,
|
|
54
|
+
safeErrorMessage,
|
|
55
|
+
sanitizeProxySettings
|
|
56
|
+
} from "./chunk-OTLSJYAC.js";
|
|
57
|
+
|
|
58
|
+
// ../../packages/adapters/src/system/managed-image.ts
|
|
59
|
+
import { posix } from "path";
|
|
60
|
+
function log(message, level = "info") {
|
|
61
|
+
return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
|
|
62
|
+
}
|
|
63
|
+
function titleCase(label) {
|
|
64
|
+
return label.charAt(0).toUpperCase() + label.slice(1);
|
|
65
|
+
}
|
|
66
|
+
var fromSourceManaged = { edge: false, mail: false };
|
|
67
|
+
function managedImagesAreFromSource(kind) {
|
|
68
|
+
return fromSourceManaged[kind];
|
|
69
|
+
}
|
|
70
|
+
function fromSourceImageMissingMessage(label, image) {
|
|
71
|
+
return `The from-source ${label} image ${image} isn't on this server \u2014 the control-plane build or transfer didn't complete (see the log above). It's an unpublished dev tag, so there's nothing to pull; fix the build and retry.`;
|
|
72
|
+
}
|
|
73
|
+
async function dockerAvailable(executor) {
|
|
74
|
+
return executor.exec("docker version --format '{{.Server.Version}}' 2>/dev/null").then((v) => Boolean(v.trim())).catch(() => false);
|
|
75
|
+
}
|
|
76
|
+
async function containerState(executor, container) {
|
|
77
|
+
const out = await executor.exec(`docker inspect -f '{{.State.Running}} {{.Config.Image}}' ${sq(container)} 2>/dev/null`).catch(() => "");
|
|
78
|
+
const line = out.split("\n").map((l) => l.trim()).find(Boolean);
|
|
79
|
+
if (!line) return null;
|
|
80
|
+
const [running, image] = line.split(" ");
|
|
81
|
+
return { running: running?.trim() === "true", image: image?.trim() || null };
|
|
82
|
+
}
|
|
83
|
+
async function containerImageRef(executor, container) {
|
|
84
|
+
return (await containerState(executor, container))?.image ?? null;
|
|
85
|
+
}
|
|
86
|
+
async function imageExistsLocally(executor, ref) {
|
|
87
|
+
const out = await executor.exec(`docker image inspect -f '{{.Id}}' ${sq(ref)} 2>/dev/null`).catch(() => "");
|
|
88
|
+
return Boolean(out.trim());
|
|
89
|
+
}
|
|
90
|
+
async function swapManagedImage(executor, opts) {
|
|
91
|
+
const { kind, from, to, label, onLog, start } = opts;
|
|
92
|
+
const Label = titleCase(label);
|
|
93
|
+
onLog(log(`Updating the ${label}: ${from} \u2192 ${to}`));
|
|
94
|
+
if (!await imageExistsLocally(executor, to)) {
|
|
95
|
+
if (managedImagesAreFromSource(kind)) {
|
|
96
|
+
onLog(log(fromSourceImageMissingMessage(label, to), "warn"));
|
|
97
|
+
return { swapped: false, down: false };
|
|
98
|
+
}
|
|
99
|
+
const pull = await executor.streamExec(`docker pull ${sq(to)}`, onLog);
|
|
100
|
+
if (pull.code !== 0) {
|
|
101
|
+
onLog(log(`Could not pull ${to} \u2014 the ${label} stays on ${from}.`, "warn"));
|
|
102
|
+
return { swapped: false, down: false };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (await start(to)) {
|
|
106
|
+
onLog(log(`${Label} updated to ${to}`));
|
|
107
|
+
return { swapped: true, down: false };
|
|
108
|
+
}
|
|
109
|
+
onLog(log(`${Label} ${to} failed to come up \u2014 rolling back to ${from}...`, "error"));
|
|
110
|
+
if (await start(from)) {
|
|
111
|
+
return { swapped: false, down: false };
|
|
112
|
+
}
|
|
113
|
+
onLog(log(`Rollback to ${from} ALSO failed \u2014 the ${label} is down on this server.`, "error"));
|
|
114
|
+
return { swapped: false, down: true };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ../../packages/adapters/src/system/proxy/takeover-journal.ts
|
|
118
|
+
var JOURNAL_DIR = "/var/lib/openship";
|
|
119
|
+
var JOURNAL_PATH = `${JOURNAL_DIR}/edge-takeover.json`;
|
|
120
|
+
function log2(message, level = "info") {
|
|
121
|
+
return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
|
|
122
|
+
}
|
|
123
|
+
async function tryExec(executor, cmd) {
|
|
124
|
+
try {
|
|
125
|
+
return await executor.exec(cmd);
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async function buildJournal(executor, status) {
|
|
131
|
+
const units = /* @__PURE__ */ new Map();
|
|
132
|
+
const containers = /* @__PURE__ */ new Map();
|
|
133
|
+
const processes = /* @__PURE__ */ new Map();
|
|
134
|
+
for (const o of status.occupants) {
|
|
135
|
+
if (o.containerName && !containers.has(o.containerName)) {
|
|
136
|
+
const r = await tryExec(
|
|
137
|
+
executor,
|
|
138
|
+
`docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' ${sq2(o.containerName)} 2>/dev/null`
|
|
139
|
+
);
|
|
140
|
+
containers.set(o.containerName, { name: o.containerName, restart: r?.trim() || "no" });
|
|
141
|
+
} else if (o.systemdUnit && !units.has(o.systemdUnit)) {
|
|
142
|
+
const en = await tryExec(executor, `systemctl is-enabled ${sq2(o.systemdUnit)} 2>/dev/null`);
|
|
143
|
+
units.set(o.systemdUnit, { unit: o.systemdUnit, wasEnabled: en?.trim() === "enabled" });
|
|
144
|
+
} else if (!o.containerName && !o.systemdUnit && o.pid && !processes.has(o.pid)) {
|
|
145
|
+
processes.set(o.pid, { pid: o.pid, command: o.rawCommand });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
150
|
+
units: [...units.values()],
|
|
151
|
+
containers: [...containers.values()],
|
|
152
|
+
processes: [...processes.values()]
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
async function writeJournal(executor, journal) {
|
|
156
|
+
try {
|
|
157
|
+
await executor.mkdir(JOURNAL_DIR);
|
|
158
|
+
await executor.writeFile(JOURNAL_PATH, JSON.stringify(journal, null, 2));
|
|
159
|
+
} catch {
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function clearJournal(executor) {
|
|
163
|
+
await tryExec(executor, `rm -f ${JOURNAL_PATH}`);
|
|
164
|
+
}
|
|
165
|
+
async function readJournal(executor) {
|
|
166
|
+
const raw = await tryExec(executor, `cat ${JOURNAL_PATH} 2>/dev/null`);
|
|
167
|
+
if (!raw?.trim()) return null;
|
|
168
|
+
try {
|
|
169
|
+
return JSON.parse(raw);
|
|
170
|
+
} catch {
|
|
171
|
+
await clearJournal(executor);
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async function rollback(executor, journal, onLog) {
|
|
176
|
+
onLog(log2("Rolling back \u2014 restoring the previous proxy...", "warn"));
|
|
177
|
+
await tryExec(executor, `docker update --restart=no ${sq2(EDGE_CONTAINER_NAME)} 2>/dev/null || true`);
|
|
178
|
+
await tryExec(executor, `docker stop ${sq2(EDGE_CONTAINER_NAME)} 2>/dev/null || true`);
|
|
179
|
+
await tryExec(
|
|
180
|
+
executor,
|
|
181
|
+
"systemctl disable --now openresty 2>/dev/null || systemctl stop openresty 2>/dev/null || true; systemctl reset-failed openresty 2>/dev/null || true"
|
|
182
|
+
);
|
|
183
|
+
for (const u of journal.units) {
|
|
184
|
+
await tryExec(
|
|
185
|
+
executor,
|
|
186
|
+
u.wasEnabled ? `systemctl enable --now ${sq2(u.unit)} 2>/dev/null || true` : `systemctl start ${sq2(u.unit)} 2>/dev/null || true`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
for (const c of journal.containers) {
|
|
190
|
+
await tryExec(executor, `docker update --restart=${sq2(c.restart)} ${sq2(c.name)} 2>/dev/null || true`);
|
|
191
|
+
await tryExec(executor, `docker start ${sq2(c.name)} 2>/dev/null || true`);
|
|
192
|
+
}
|
|
193
|
+
for (const p of journal.processes ?? []) {
|
|
194
|
+
if (p.command) {
|
|
195
|
+
await tryExec(
|
|
196
|
+
executor,
|
|
197
|
+
`setsid -f sh -c ${sq2(p.command)} 2>/dev/null || (nohup sh -c ${sq2(p.command)} >/dev/null 2>&1 &) || true`
|
|
198
|
+
);
|
|
199
|
+
} else {
|
|
200
|
+
onLog(log2(`Could not restore process ${p.pid} \u2014 no command captured.`, "warn"));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const restored = await portIsServed(executor, 80);
|
|
204
|
+
if (!restored) {
|
|
205
|
+
onLog(
|
|
206
|
+
log2(
|
|
207
|
+
"Nothing is listening on :80 after the restore \u2014 the box is NOT serving. Start your proxy by hand (e.g. `systemctl start nginx`, or `docker start <name>`).",
|
|
208
|
+
"error"
|
|
209
|
+
)
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
return restored;
|
|
213
|
+
}
|
|
214
|
+
async function portIsServed(executor, port) {
|
|
215
|
+
const hex = port.toString(16).toUpperCase().padStart(4, "0");
|
|
216
|
+
const out = await tryExec(
|
|
217
|
+
executor,
|
|
218
|
+
`(command -v ss >/dev/null 2>&1 && ss -ltn 2>/dev/null | grep -qE ':${port}[[:space:]]' && echo yes) || (grep -qiE '^[[:space:]]*[0-9]+:[[:space:]]*[0-9A-F]{8}:${hex}[[:space:]]+[0-9A-F]{8}:0000[[:space:]]+0A' /proc/net/tcp 2>/dev/null && echo yes) || true`
|
|
219
|
+
);
|
|
220
|
+
return (out ?? "").includes("yes");
|
|
221
|
+
}
|
|
222
|
+
async function beginEdgeTakeover(executor, status, onLog, targets) {
|
|
223
|
+
await writeJournal(executor, await buildJournal(executor, status));
|
|
224
|
+
return freeEdgeTargets(
|
|
225
|
+
executor,
|
|
226
|
+
targets?.length ? targets : stopTargetsForStatus(status),
|
|
227
|
+
(m, l) => onLog(log2(m, l))
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
async function rollbackEdgeTakeover(executor, onLog) {
|
|
231
|
+
const journal = await readJournal(executor);
|
|
232
|
+
if (!journal || journal.completed) return false;
|
|
233
|
+
const restored = await rollback(executor, journal, onLog);
|
|
234
|
+
await clearJournal(executor);
|
|
235
|
+
return restored;
|
|
236
|
+
}
|
|
237
|
+
async function completeEdgeTakeover(executor) {
|
|
238
|
+
const journal = await readJournal(executor);
|
|
239
|
+
if (journal) {
|
|
240
|
+
journal.completed = true;
|
|
241
|
+
await writeJournal(executor, journal);
|
|
242
|
+
}
|
|
243
|
+
await clearJournal(executor);
|
|
244
|
+
}
|
|
245
|
+
async function recoverInterruptedTakeover(executor, onLog, isEdgeHealthy) {
|
|
246
|
+
const journal = await readJournal(executor);
|
|
247
|
+
if (!journal) return;
|
|
248
|
+
if (journal.completed) {
|
|
249
|
+
await clearJournal(executor);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (isEdgeHealthy && await isEdgeHealthy()) {
|
|
253
|
+
onLog(log2("Found an unfinished edge takeover, but our edge is serving \u2014 keeping it.", "warn"));
|
|
254
|
+
await clearJournal(executor);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
onLog(log2("Found an interrupted edge takeover \u2014 restoring the previous proxy.", "warn"));
|
|
258
|
+
await rollback(executor, journal, onLog);
|
|
259
|
+
await clearJournal(executor);
|
|
260
|
+
}
|
|
52
261
|
|
|
53
262
|
// ../../packages/adapters/src/system/proxy/import/parse-utils.ts
|
|
54
|
-
async function
|
|
263
|
+
async function tryExec2(executor, command) {
|
|
55
264
|
try {
|
|
56
265
|
return await executor.exec(command);
|
|
57
266
|
} catch {
|
|
@@ -59,7 +268,21 @@ async function tryExec(executor, command) {
|
|
|
59
268
|
}
|
|
60
269
|
}
|
|
61
270
|
function stripComments(text) {
|
|
62
|
-
return text.
|
|
271
|
+
return text.split("\n").map((line) => {
|
|
272
|
+
let quote;
|
|
273
|
+
for (let i = 0; i < line.length; i++) {
|
|
274
|
+
const ch = line[i];
|
|
275
|
+
if (quote) {
|
|
276
|
+
if (ch === "\\") i++;
|
|
277
|
+
else if (ch === quote) quote = void 0;
|
|
278
|
+
} else if (ch === '"' || ch === "'") {
|
|
279
|
+
quote = ch;
|
|
280
|
+
} else if (ch === "#") {
|
|
281
|
+
return line.slice(0, i);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return line;
|
|
285
|
+
}).join("\n");
|
|
63
286
|
}
|
|
64
287
|
function extractBlocks(text, keyword) {
|
|
65
288
|
const blocks = [];
|
|
@@ -82,15 +305,40 @@ function extractBlocks(text, keyword) {
|
|
|
82
305
|
}
|
|
83
306
|
return blocks;
|
|
84
307
|
}
|
|
308
|
+
function collapseByHost(items, hostsOf, score) {
|
|
309
|
+
const groups = /* @__PURE__ */ new Map();
|
|
310
|
+
for (const item of items) {
|
|
311
|
+
const key = [...hostsOf(item)].sort().join(" ");
|
|
312
|
+
const bag = groups.get(key);
|
|
313
|
+
if (bag) bag.push(item);
|
|
314
|
+
else groups.set(key, [item]);
|
|
315
|
+
}
|
|
316
|
+
const kept = [];
|
|
317
|
+
const dropped = [];
|
|
318
|
+
for (const bag of groups.values()) {
|
|
319
|
+
let winner = bag[0];
|
|
320
|
+
for (const cand of bag) if (score(cand) > score(winner)) winner = cand;
|
|
321
|
+
kept.push(winner);
|
|
322
|
+
for (const loser of bag) if (loser !== winner) dropped.push(loser);
|
|
323
|
+
}
|
|
324
|
+
return { kept, dropped };
|
|
325
|
+
}
|
|
85
326
|
function sq3(value) {
|
|
86
327
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
87
328
|
}
|
|
88
329
|
|
|
89
330
|
// ../../packages/adapters/src/system/proxy/import/nginx.ts
|
|
331
|
+
async function dumpResolvedConfig(executor, bins) {
|
|
332
|
+
for (const bin of bins) {
|
|
333
|
+
const dumped = await tryExec2(executor, `${bin} -T 2>/dev/null`);
|
|
334
|
+
if (dumped && /server\s*\{/.test(dumped)) return dumped;
|
|
335
|
+
}
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
90
338
|
async function loadNginxConfig(executor) {
|
|
91
|
-
const dumped = await
|
|
92
|
-
if (dumped
|
|
93
|
-
const cat = await
|
|
339
|
+
const dumped = await dumpResolvedConfig(executor, ["nginx", "openresty"]);
|
|
340
|
+
if (dumped) return dumped;
|
|
341
|
+
const cat = await tryExec2(
|
|
94
342
|
executor,
|
|
95
343
|
"cat /etc/nginx/nginx.conf /etc/nginx/sites-enabled/* /etc/nginx/conf.d/*.conf 2>/dev/null"
|
|
96
344
|
);
|
|
@@ -122,10 +370,13 @@ function resolveProxyTarget(proxyPass, upstreams) {
|
|
|
122
370
|
const host = authority.replace(/:\d+$/, "");
|
|
123
371
|
if (upstreams.has(host)) return { url: `${scheme}${upstreams.get(host)}` };
|
|
124
372
|
const isIp = /^\d{1,3}(\.\d{1,3}){3}$/.test(host);
|
|
125
|
-
if (
|
|
373
|
+
if (host === "localhost" || host === "ip6-localhost") {
|
|
374
|
+
return { url: raw.replace(/^(https?:\/\/)[^/:]+/i, "$1127.0.0.1") };
|
|
375
|
+
}
|
|
376
|
+
if (isIp || host.includes(".")) return { url: raw };
|
|
126
377
|
return { reason: `proxy_pass host "${host}" is an undeclared upstream \u2014 not migratable` };
|
|
127
378
|
}
|
|
128
|
-
function
|
|
379
|
+
function locationBlocks(serverBody) {
|
|
129
380
|
const out = [];
|
|
130
381
|
const re = /(?:^|[\s;}])location\s+([^{]+?)\s*\{/g;
|
|
131
382
|
let m;
|
|
@@ -139,13 +390,26 @@ function extractLocationProxies(serverBody) {
|
|
|
139
390
|
else if (serverBody[i] === "}") depth--;
|
|
140
391
|
}
|
|
141
392
|
if (depth !== 0) break;
|
|
142
|
-
|
|
393
|
+
out.push({ path, body: serverBody.slice(openIdx + 1, i - 1) });
|
|
143
394
|
re.lastIndex = i;
|
|
144
|
-
const pp = firstDirective(locBody, "proxy_pass");
|
|
145
|
-
if (pp) out.push({ path, proxyPass: pp });
|
|
146
395
|
}
|
|
147
396
|
return out;
|
|
148
397
|
}
|
|
398
|
+
function extractLocationProxies(serverBody) {
|
|
399
|
+
const out = [];
|
|
400
|
+
for (const loc of locationBlocks(serverBody)) {
|
|
401
|
+
const pp = firstDirective(loc.body, "proxy_pass");
|
|
402
|
+
if (pp) out.push({ path: loc.path, proxyPass: pp });
|
|
403
|
+
}
|
|
404
|
+
return out;
|
|
405
|
+
}
|
|
406
|
+
function withoutQuotedValues(body) {
|
|
407
|
+
return body.replace(/'[^'\n]*'/g, "''").replace(/"[^"\n]*"/g, '""');
|
|
408
|
+
}
|
|
409
|
+
var SERVING_DIRECTIVE_RE = /(?:^|[;{}\s])(?:proxy_pass|root|alias|fastcgi_pass|uwsgi_pass|scgi_pass|grpc_pass)\s/;
|
|
410
|
+
function servesNothing(locationBody) {
|
|
411
|
+
return !SERVING_DIRECTIVE_RE.test(withoutQuotedValues(locationBody));
|
|
412
|
+
}
|
|
149
413
|
function isHttpsUpgradeForSelf(body, names) {
|
|
150
414
|
const targets = [
|
|
151
415
|
...body.matchAll(/(?:^|[\s;{])return\s+30[1-8]\s+([^;]+);/g),
|
|
@@ -160,6 +424,32 @@ function isHttpsUpgradeForSelf(body, names) {
|
|
|
160
424
|
return names.includes(host);
|
|
161
425
|
});
|
|
162
426
|
}
|
|
427
|
+
var CHALLENGE_LOCATION_RE = /\.well-known\/(acme-challenge|oblien-proxy-challenge)/i;
|
|
428
|
+
function isAcmeWebrootOnly(body) {
|
|
429
|
+
const locations = locationBlocks(body);
|
|
430
|
+
if (locations.length === 0) {
|
|
431
|
+
const root = firstDirective(body, "root") ?? "";
|
|
432
|
+
return /letsencrypt|acme|certbot/i.test(root);
|
|
433
|
+
}
|
|
434
|
+
if (!locations.some((l) => CHALLENGE_LOCATION_RE.test(l.path))) return false;
|
|
435
|
+
return locations.every((l) => CHALLENGE_LOCATION_RE.test(l.path) || servesNothing(l.body));
|
|
436
|
+
}
|
|
437
|
+
function parseProxyDirectives(body) {
|
|
438
|
+
const raw = {};
|
|
439
|
+
const candidate = {};
|
|
440
|
+
for (const spec of PROXY_DIRECTIVES) {
|
|
441
|
+
const found = firstDirective(body, spec.directive);
|
|
442
|
+
if (found === void 0) continue;
|
|
443
|
+
raw[spec.key] = found;
|
|
444
|
+
const parsed = parseProxyValue(spec, found);
|
|
445
|
+
if (parsed !== void 0) candidate[spec.key] = parsed;
|
|
446
|
+
}
|
|
447
|
+
const settings = sanitizeProxySettings(candidate);
|
|
448
|
+
return {
|
|
449
|
+
...settings ? { settings } : {},
|
|
450
|
+
...Object.keys(raw).length > 0 ? { raw } : {}
|
|
451
|
+
};
|
|
452
|
+
}
|
|
163
453
|
function parseServer(body, source, upstreams) {
|
|
164
454
|
const warnings = [];
|
|
165
455
|
const names = firstDirective(body, "server_name")?.split(/\s+/).filter((n) => n && n !== "_" && !n.startsWith("~")) ?? [];
|
|
@@ -186,10 +476,12 @@ function parseServer(body, source, upstreams) {
|
|
|
186
476
|
const primary = resolved.find((r) => r.path === "/") ?? resolved[0];
|
|
187
477
|
target = { kind: "proxy", url: primary.url };
|
|
188
478
|
routes = resolved;
|
|
189
|
-
} else if (root) {
|
|
190
|
-
target = { kind: "static", root: root.replace(/;$/, "") };
|
|
191
479
|
} else if (isHttpsUpgradeForSelf(body, names)) {
|
|
192
480
|
return { warnings: [] };
|
|
481
|
+
} else if (root && !isAcmeWebrootOnly(body)) {
|
|
482
|
+
target = { kind: "static", root: root.replace(/;$/, "") };
|
|
483
|
+
} else if (root) {
|
|
484
|
+
return { warnings: [] };
|
|
193
485
|
} else {
|
|
194
486
|
warnings.push(`nginx: ${names[0]} has neither proxy_pass nor root \u2014 skipped (${source})`);
|
|
195
487
|
return { warnings };
|
|
@@ -197,6 +489,9 @@ function parseServer(body, source, upstreams) {
|
|
|
197
489
|
const site = { serverNames: names, ssl, target, source };
|
|
198
490
|
if (routes) site.routes = routes;
|
|
199
491
|
if (certPath && keyPath) site.tls = { certPath, keyPath };
|
|
492
|
+
const tunables = parseProxyDirectives(body);
|
|
493
|
+
if (tunables.settings) site.proxy = tunables.settings;
|
|
494
|
+
if (tunables.raw) site.proxyRaw = tunables.raw;
|
|
200
495
|
return { site, warnings };
|
|
201
496
|
}
|
|
202
497
|
function parseNginxConfig(raw) {
|
|
@@ -216,7 +511,12 @@ function parseNginxConfig(raw) {
|
|
|
216
511
|
warnings.push(...blockWarnings);
|
|
217
512
|
if (site) sites.push(site);
|
|
218
513
|
}
|
|
219
|
-
|
|
514
|
+
const { kept } = collapseByHost(
|
|
515
|
+
sites,
|
|
516
|
+
(s) => s.serverNames,
|
|
517
|
+
(s) => (s.ssl ? 2 : 0) + (s.target.kind === "proxy" ? 1 : 0)
|
|
518
|
+
);
|
|
519
|
+
return { proxy: "nginx", sites: kept, warnings };
|
|
220
520
|
}
|
|
221
521
|
async function scanNginx(executor) {
|
|
222
522
|
return parseNginxConfig(await loadNginxConfig(executor));
|
|
@@ -226,42 +526,90 @@ var OUR_EDGE_SITE_GLOBS = [
|
|
|
226
526
|
`${OPENRESTY_DEFAULT_PATHS.sitesDir}/*.conf`,
|
|
227
527
|
"/etc/openresty/sites-enabled/*.conf"
|
|
228
528
|
];
|
|
529
|
+
async function scanForeignOpenResty(executor) {
|
|
530
|
+
const dump = await dumpResolvedConfig(executor, ["openresty"]);
|
|
531
|
+
if (dump) return parseNginxConfig(dump);
|
|
532
|
+
return scanOpenshipEdge(executor);
|
|
533
|
+
}
|
|
229
534
|
async function scanOpenshipEdge(executor) {
|
|
230
535
|
const cat = `cat ${OUR_EDGE_SITE_GLOBS.join(" ")} 2>/dev/null`;
|
|
231
|
-
const raw = await
|
|
536
|
+
const raw = await tryExec2(executor, cat);
|
|
232
537
|
if (raw?.trim()) return parseNginxConfig(raw);
|
|
233
538
|
const container = await resolveOurEdgeContainer(executor);
|
|
234
539
|
if (!container) return parseNginxConfig("");
|
|
235
|
-
const fromContainer = await
|
|
540
|
+
const fromContainer = await tryExec2(executor, containerCommand(container, cat));
|
|
236
541
|
return parseNginxConfig(fromContainer ?? "");
|
|
237
542
|
}
|
|
238
543
|
|
|
239
544
|
// ../../packages/adapters/src/system/proxy/import/caddy.ts
|
|
240
545
|
var CADDYFILE_PATHS = ["/etc/caddy/Caddyfile", "/etc/Caddyfile"];
|
|
546
|
+
async function loadCaddyAdminConfig(executor) {
|
|
547
|
+
const endpoint = "http://localhost:2019/config/";
|
|
548
|
+
const out = await tryExec2(executor, `curl -fsS ${endpoint} 2>/dev/null`) || await tryExec2(executor, `wget -qO- ${endpoint} 2>/dev/null`);
|
|
549
|
+
return out && out.trim().startsWith("{") ? out : null;
|
|
550
|
+
}
|
|
241
551
|
async function loadCaddyJson(executor) {
|
|
242
552
|
for (const p of CADDYFILE_PATHS) {
|
|
243
|
-
const out = await
|
|
553
|
+
const out = await tryExec2(executor, `caddy adapt --adapter caddyfile --config ${p} 2>/dev/null`);
|
|
244
554
|
if (out && out.trim().startsWith("{")) return out;
|
|
245
555
|
}
|
|
246
556
|
return null;
|
|
247
557
|
}
|
|
248
|
-
function flattenHandlers(handlers, out = []) {
|
|
558
|
+
function flattenHandlers(handlers, scoped = false, out = []) {
|
|
249
559
|
for (const h of handlers ?? []) {
|
|
250
560
|
if (h.handler === "subroute") {
|
|
251
|
-
for (const r of h.routes ?? [])
|
|
561
|
+
for (const r of h.routes ?? []) {
|
|
562
|
+
flattenHandlers(r.handle, scoped || (r.match?.length ?? 0) > 0, out);
|
|
563
|
+
}
|
|
252
564
|
} else {
|
|
253
|
-
out.push(h);
|
|
565
|
+
out.push({ h, scoped });
|
|
254
566
|
}
|
|
255
567
|
}
|
|
256
568
|
return out;
|
|
257
569
|
}
|
|
570
|
+
var UNMIGRATABLE_HANDLERS = {
|
|
571
|
+
php_fastcgi: "a PHP-FPM site (php_fastcgi)",
|
|
572
|
+
fastcgi: "a FastCGI backend",
|
|
573
|
+
static_response: "a static inline response (respond)",
|
|
574
|
+
templates: "Caddy templates",
|
|
575
|
+
encode: "",
|
|
576
|
+
headers: "",
|
|
577
|
+
authentication: "Caddy authentication",
|
|
578
|
+
rewrite: "",
|
|
579
|
+
acme_server: "an embedded ACME server"
|
|
580
|
+
};
|
|
258
581
|
function targetFromRoute(route) {
|
|
259
582
|
const flat = flattenHandlers(route.handle);
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
if (
|
|
264
|
-
|
|
583
|
+
const proxies = flat.filter((f) => f.h.handler === "reverse_proxy");
|
|
584
|
+
const chosen = proxies.find((f) => !f.scoped) ?? proxies[0];
|
|
585
|
+
const proxy = chosen?.h;
|
|
586
|
+
if (proxy?.transport?.protocol === "fastcgi") {
|
|
587
|
+
return { note: "serves a PHP-FPM/FastCGI site (php_fastcgi) \u2014 not an HTTP upstream" };
|
|
588
|
+
}
|
|
589
|
+
const pathScopedOnly = Boolean(chosen?.scoped);
|
|
590
|
+
const hasScopedSibling = proxies.some((f) => f.scoped);
|
|
591
|
+
const dials = (proxy?.upstreams ?? []).map((u) => u.dial).filter((d) => Boolean(d));
|
|
592
|
+
if (dials.length > 0) {
|
|
593
|
+
const dial = dials[0];
|
|
594
|
+
if (/^unix\//i.test(dial)) {
|
|
595
|
+
return { note: `proxies to the unix socket "${dial}", which needs a manual upstream` };
|
|
596
|
+
}
|
|
597
|
+
return {
|
|
598
|
+
target: { kind: "proxy", url: /^https?:\/\//.test(dial) ? dial : `http://${dial}` },
|
|
599
|
+
extraUpstreams: dials.length - 1,
|
|
600
|
+
pathScopedOnly,
|
|
601
|
+
hasScopedSibling
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
const root = flat.find((f) => f.h.handler === "vars" && typeof f.h.root === "string")?.h.root;
|
|
605
|
+
if (root && flat.some((f) => f.h.handler === "file_server")) {
|
|
606
|
+
return { target: { kind: "static", root } };
|
|
607
|
+
}
|
|
608
|
+
for (const { h } of flat) {
|
|
609
|
+
const label = h.handler ? UNMIGRATABLE_HANDLERS[h.handler] : void 0;
|
|
610
|
+
if (label) return { note: `serves ${label}` };
|
|
611
|
+
}
|
|
612
|
+
return {};
|
|
265
613
|
}
|
|
266
614
|
function parseCaddyJson(raw) {
|
|
267
615
|
let cfg;
|
|
@@ -273,21 +621,51 @@ function parseCaddyJson(raw) {
|
|
|
273
621
|
const servers = cfg.apps?.http?.servers;
|
|
274
622
|
if (!servers) return null;
|
|
275
623
|
const sites = [];
|
|
624
|
+
const warnings = [];
|
|
276
625
|
for (const srv of Object.values(servers)) {
|
|
277
626
|
const ssl = (srv.listen ?? []).some((l) => l.endsWith(":443"));
|
|
278
627
|
for (const route of srv.routes ?? []) {
|
|
279
628
|
const hosts = (route.match ?? []).flatMap((m) => m.host ?? []).filter((h) => h && h !== "*");
|
|
280
629
|
if (hosts.length === 0) continue;
|
|
281
|
-
const
|
|
282
|
-
if (
|
|
283
|
-
|
|
630
|
+
const outcome = targetFromRoute(route);
|
|
631
|
+
if (outcome.note) {
|
|
632
|
+
warnings.push(`caddy: ${hosts.join(", ")} ${outcome.note} \u2014 re-add it manually`);
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
if (!outcome.target) continue;
|
|
636
|
+
if (outcome.extraUpstreams) {
|
|
637
|
+
warnings.push(
|
|
638
|
+
`caddy: ${hosts.join(", ")} load-balances across ${outcome.extraUpstreams + 1} upstreams \u2014 only the first is migrated`
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
if (outcome.hasScopedSibling || (route.match ?? []).some((m) => m.path?.length)) {
|
|
642
|
+
warnings.push(
|
|
643
|
+
`caddy: ${hosts.join(", ")} routes some paths to other upstreams \u2014 only the catch-all is migrated; re-add path rules manually`
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
if (outcome.pathScopedOnly) {
|
|
647
|
+
warnings.push(
|
|
648
|
+
`caddy: ${hosts.join(", ")} has no catch-all upstream \u2014 used its path-scoped one; verify the target`
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
sites.push({ serverNames: hosts, ssl, target: outcome.target, source: "caddy (adapt)" });
|
|
284
652
|
}
|
|
285
653
|
}
|
|
286
|
-
|
|
654
|
+
const { kept, dropped } = collapseByHost(
|
|
655
|
+
sites,
|
|
656
|
+
(s) => s.serverNames,
|
|
657
|
+
(s) => s.ssl ? 1 : 0
|
|
658
|
+
);
|
|
659
|
+
for (const d of dropped) {
|
|
660
|
+
warnings.push(
|
|
661
|
+
`caddy: ${d.serverNames.join(", ")} had a second (non-TLS) block \u2014 kept the TLS one`
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
return { sites: kept, warnings };
|
|
287
665
|
}
|
|
288
666
|
async function loadCaddyfile(executor) {
|
|
289
667
|
for (const p of CADDYFILE_PATHS) {
|
|
290
|
-
const out = await
|
|
668
|
+
const out = await tryExec2(executor, `cat ${p} 2>/dev/null`);
|
|
291
669
|
if (out && out.trim()) return out;
|
|
292
670
|
}
|
|
293
671
|
return "";
|
|
@@ -324,10 +702,11 @@ function parseAddresses(header) {
|
|
|
324
702
|
}).filter((a) => a.host && !a.host.startsWith(":") && a.host !== "*");
|
|
325
703
|
}
|
|
326
704
|
async function scanCaddy(executor) {
|
|
327
|
-
const
|
|
328
|
-
|
|
705
|
+
for (const load of [loadCaddyAdminConfig, loadCaddyJson]) {
|
|
706
|
+
const json = await load(executor);
|
|
707
|
+
if (!json) continue;
|
|
329
708
|
const parsed = parseCaddyJson(json);
|
|
330
|
-
if (parsed && parsed.sites.length > 0) {
|
|
709
|
+
if (parsed && (parsed.sites.length > 0 || parsed.warnings.length > 0)) {
|
|
331
710
|
return { proxy: "caddy", sites: parsed.sites, warnings: parsed.warnings };
|
|
332
711
|
}
|
|
333
712
|
}
|
|
@@ -357,19 +736,44 @@ async function scanCaddyText(executor) {
|
|
|
357
736
|
warnings.push(`caddy: skipped a block with no usable address (${header.slice(0, 40)})`);
|
|
358
737
|
continue;
|
|
359
738
|
}
|
|
360
|
-
const
|
|
739
|
+
const inlineProxy = body.match(/(?:^|\s)reverse_proxy\s+([^\n{]+)/);
|
|
740
|
+
const blockProxy = body.match(/(?:^|\s)reverse_proxy\s*\{([\s\S]*?)\}/);
|
|
741
|
+
const blockTo = blockProxy?.[1].match(/(?:^|\n)\s*to\s+([^\n]+)/);
|
|
361
742
|
const rootMatch = body.match(/(?:^|\s)root\s+\*?\s*([^\n\s]+)/);
|
|
362
743
|
const tlsMatch = body.match(/(?:^|\s)tls\s+(\S+)\s+(\S+)/);
|
|
744
|
+
const upstreams = (inlineProxy?.[1] ?? blockTo?.[1] ?? "").trim().split(/\s+/).filter(Boolean);
|
|
745
|
+
const host = addrs[0].host;
|
|
363
746
|
let target;
|
|
364
|
-
if (
|
|
365
|
-
const upstream =
|
|
747
|
+
if (upstreams.length > 0) {
|
|
748
|
+
const upstream = upstreams[0];
|
|
749
|
+
if (/^unix\//i.test(upstream)) {
|
|
750
|
+
warnings.push(
|
|
751
|
+
`caddy: ${host} proxies to the unix socket "${upstream}" \u2014 needs a manual upstream, skipped`
|
|
752
|
+
);
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
if (upstreams.length > 1) {
|
|
756
|
+
warnings.push(
|
|
757
|
+
`caddy: ${host} load-balances across ${upstreams.length} upstreams \u2014 only the first is migrated`
|
|
758
|
+
);
|
|
759
|
+
}
|
|
366
760
|
target = { kind: "proxy", url: /^https?:\/\//.test(upstream) ? upstream : `http://${upstream}` };
|
|
761
|
+
} else if (/(?:^|\s)php_fastcgi\s/.test(body)) {
|
|
762
|
+
warnings.push(
|
|
763
|
+
`caddy: ${host} serves a PHP-FPM site (php_fastcgi) \u2014 not migratable as a static root; re-add it manually`
|
|
764
|
+
);
|
|
765
|
+
continue;
|
|
367
766
|
} else if (rootMatch) {
|
|
368
767
|
target = { kind: "static", root: rootMatch[1] };
|
|
369
768
|
} else {
|
|
370
|
-
warnings.push(`caddy: ${
|
|
769
|
+
warnings.push(`caddy: ${host} has no reverse_proxy or root \u2014 skipped`);
|
|
371
770
|
continue;
|
|
372
771
|
}
|
|
772
|
+
if (/(?:^|\s)(handle_path|handle)\s+\//.test(body)) {
|
|
773
|
+
warnings.push(
|
|
774
|
+
`caddy: ${host} routes by path (handle/handle_path) \u2014 only one upstream is migrated; re-add path rules manually`
|
|
775
|
+
);
|
|
776
|
+
}
|
|
373
777
|
const site = {
|
|
374
778
|
serverNames: addrs.map((a) => a.host),
|
|
375
779
|
ssl: addrs.some((a) => a.ssl),
|
|
@@ -381,12 +785,22 @@ async function scanCaddyText(executor) {
|
|
|
381
785
|
}
|
|
382
786
|
sites.push(site);
|
|
383
787
|
}
|
|
384
|
-
|
|
788
|
+
const { kept, dropped } = collapseByHost(
|
|
789
|
+
sites,
|
|
790
|
+
(s) => s.serverNames,
|
|
791
|
+
(s) => s.ssl ? 1 : 0
|
|
792
|
+
);
|
|
793
|
+
for (const d of dropped) {
|
|
794
|
+
warnings.push(
|
|
795
|
+
`caddy: ${d.serverNames.join(", ")} had a second (non-TLS) block \u2014 kept the TLS one`
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
return { proxy: "caddy", sites: kept, warnings };
|
|
385
799
|
}
|
|
386
800
|
|
|
387
801
|
// ../../packages/adapters/src/system/proxy/import/apache.ts
|
|
388
802
|
async function vhostFilesFromDump(executor) {
|
|
389
|
-
const dump = await
|
|
803
|
+
const dump = await tryExec2(
|
|
390
804
|
executor,
|
|
391
805
|
"apachectl -S 2>/dev/null || apache2ctl -S 2>/dev/null || httpd -S 2>/dev/null"
|
|
392
806
|
);
|
|
@@ -401,42 +815,107 @@ async function vhostFilesFromDump(executor) {
|
|
|
401
815
|
async function loadApacheConfig(executor) {
|
|
402
816
|
const files = await vhostFilesFromDump(executor);
|
|
403
817
|
if (files.length > 0) {
|
|
404
|
-
const cat2 = await
|
|
818
|
+
const cat2 = await tryExec2(executor, `cat ${files.map(sq3).join(" ")} 2>/dev/null`);
|
|
405
819
|
if (cat2 && cat2.trim()) return cat2;
|
|
406
820
|
}
|
|
407
|
-
const cat = await
|
|
821
|
+
const cat = await tryExec2(
|
|
408
822
|
executor,
|
|
409
823
|
"cat /etc/apache2/sites-enabled/*.conf /etc/apache2/apache2.conf /etc/httpd/conf.d/*.conf /etc/httpd/conf/httpd.conf 2>/dev/null"
|
|
410
824
|
);
|
|
411
825
|
return cat ?? "";
|
|
412
826
|
}
|
|
827
|
+
function unquote(value) {
|
|
828
|
+
return value.replace(/^["']|["']$/g, "");
|
|
829
|
+
}
|
|
413
830
|
function directive(body, name) {
|
|
414
831
|
const m = body.match(new RegExp(`(?:^|\\n)\\s*${name}\\s+([^\\n]+)`, "i"));
|
|
415
|
-
|
|
832
|
+
const value = m?.[1]?.trim();
|
|
833
|
+
return value === void 0 ? void 0 : unquote(value);
|
|
416
834
|
}
|
|
417
835
|
function directiveAll(body, name) {
|
|
418
836
|
return [...body.matchAll(new RegExp(`(?:^|\\n)\\s*${name}\\s+([^\\n]+)`, "gi"))].map(
|
|
419
837
|
(m) => m[1].trim()
|
|
420
838
|
);
|
|
421
839
|
}
|
|
840
|
+
function isHttpsRedirectOnly(body) {
|
|
841
|
+
const hasProxy = /(?:^|\n)\s*ProxyPass\s+\S+\s+(?!!)\S/i.test(body);
|
|
842
|
+
if (hasProxy) return false;
|
|
843
|
+
const redirects = [
|
|
844
|
+
...body.matchAll(/(?:^|\n)\s*Redirect(?:Permanent|Match)?\s+(?:\d{3}\s+|permanent\s+|temp\s+)?\S*\s*(\S+)/gi),
|
|
845
|
+
...body.matchAll(/(?:^|\n)\s*RewriteRule\s+\S+\s+(\S+)\s*\[[^\]]*R=?3\d\d[^\]]*\]/gi)
|
|
846
|
+
].map((m) => m[1]);
|
|
847
|
+
return redirects.length > 0 && redirects.some((t) => /^https:\/\//i.test(t));
|
|
848
|
+
}
|
|
849
|
+
function hostOnly(name) {
|
|
850
|
+
return unquote(name.trim()).replace(/^https?:\/\//i, "").replace(/:\d+$/, "");
|
|
851
|
+
}
|
|
852
|
+
function proxyRoutes(body) {
|
|
853
|
+
const routes = [];
|
|
854
|
+
const scopes = [];
|
|
855
|
+
let regexScoped = false;
|
|
856
|
+
for (const line of body.split("\n")) {
|
|
857
|
+
const open = line.match(/^\s*<Location(Match)?\s+([^>]*)>/i);
|
|
858
|
+
if (open) {
|
|
859
|
+
const regex = Boolean(open[1]);
|
|
860
|
+
const arg = unquote(open[2].trim());
|
|
861
|
+
scopes.push({ path: !regex && arg.startsWith("/") ? arg : null, regex });
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
if (/^\s*<\/Location(?:Match)?\s*>/i.test(line)) {
|
|
865
|
+
scopes.pop();
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
const m = line.match(/^\s*ProxyPass\s+(\S+)(?:\s+(\S+))?/i);
|
|
869
|
+
if (!m) continue;
|
|
870
|
+
const first = unquote(m[1]);
|
|
871
|
+
if (first.startsWith("/")) {
|
|
872
|
+
routes.push({ path: first, url: m[2] ? unquote(m[2]) : "" });
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
const scope = scopes[scopes.length - 1];
|
|
876
|
+
if (scope?.path) routes.push({ path: scope.path, url: first });
|
|
877
|
+
else if (scope?.regex) regexScoped = true;
|
|
878
|
+
}
|
|
879
|
+
return { routes, regexScoped };
|
|
880
|
+
}
|
|
422
881
|
function parseVhost(body, portHint) {
|
|
423
882
|
const serverName = directive(body, "ServerName")?.split(/\s+/)[0];
|
|
424
883
|
const aliases = directiveAll(body, "ServerAlias").flatMap((line) => line.split(/\s+/));
|
|
425
|
-
const names = [serverName, ...aliases].filter((n) => Boolean(n));
|
|
884
|
+
const names = [serverName, ...aliases].filter((n) => Boolean(n)).map(hostOnly).filter(Boolean);
|
|
426
885
|
if (names.length === 0) {
|
|
427
886
|
return { warning: "apache: skipped a VirtualHost with no ServerName" };
|
|
428
887
|
}
|
|
429
|
-
|
|
430
|
-
const
|
|
888
|
+
if (isHttpsRedirectOnly(body)) return null;
|
|
889
|
+
const proxy = proxyRoutes(body);
|
|
890
|
+
const routes = proxy.routes.filter((p) => p.url && p.url !== "!");
|
|
891
|
+
const docRoot = directive(body, "DocumentRoot");
|
|
892
|
+
const fcgiMatch = /(?:^|\n)\s*ProxyPassMatch\s+[^\n]*(fcgi:|php-fpm|php\d*-fpm|\.sock)/i.test(body);
|
|
431
893
|
const certPath = directive(body, "SSLCertificateFile");
|
|
432
894
|
const keyPath = directive(body, "SSLCertificateKeyFile");
|
|
433
895
|
const ssl = /:443/.test(portHint) || /SSLEngine\s+on/i.test(body) || Boolean(certPath);
|
|
434
896
|
let target;
|
|
435
897
|
if (routes.length > 0) {
|
|
436
898
|
const primary = routes.find((r) => r.path === "/") ?? routes[0];
|
|
899
|
+
if (/^balancer:\/\//i.test(primary.url)) {
|
|
900
|
+
return {
|
|
901
|
+
warning: `apache: ${names[0]} proxies to an mod_proxy_balancer cluster (${primary.url}) \u2014 re-add its members manually`
|
|
902
|
+
};
|
|
903
|
+
}
|
|
437
904
|
target = { kind: "proxy", url: primary.url };
|
|
905
|
+
} else if (fcgiMatch) {
|
|
906
|
+
return {
|
|
907
|
+
warning: `apache: ${names[0]} is a PHP-FPM site (ProxyPassMatch \u2192 FastCGI) \u2014 not migratable as a static root; re-add it manually`
|
|
908
|
+
};
|
|
438
909
|
} else if (docRoot) {
|
|
439
910
|
target = { kind: "static", root: docRoot };
|
|
911
|
+
} else if (/(?:^|\n)\s*ProxyPassMatch\s/i.test(body)) {
|
|
912
|
+
return {
|
|
913
|
+
warning: `apache: ${names[0]} uses ProxyPassMatch (regex proxying) \u2014 re-add it manually`
|
|
914
|
+
};
|
|
915
|
+
} else if (proxy.regexScoped) {
|
|
916
|
+
return {
|
|
917
|
+
warning: `apache: ${names[0]} proxies from inside a <LocationMatch> (regex proxying) \u2014 re-add it manually`
|
|
918
|
+
};
|
|
440
919
|
} else {
|
|
441
920
|
return { warning: `apache: ${names[0]} has neither ProxyPass nor DocumentRoot \u2014 skipped` };
|
|
442
921
|
}
|
|
@@ -445,6 +924,11 @@ function parseVhost(body, portHint) {
|
|
|
445
924
|
if (certPath && keyPath) site.tls = { certPath, keyPath };
|
|
446
925
|
return site;
|
|
447
926
|
}
|
|
927
|
+
function redirectVhostNames(body) {
|
|
928
|
+
const serverName = directive(body, "ServerName")?.split(/\s+/)[0];
|
|
929
|
+
const aliases = directiveAll(body, "ServerAlias").flatMap((l) => l.split(/\s+/));
|
|
930
|
+
return [serverName, ...aliases].filter((n) => Boolean(n)).map(hostOnly).filter(Boolean);
|
|
931
|
+
}
|
|
448
932
|
function vhostBlocks(text) {
|
|
449
933
|
const out = [];
|
|
450
934
|
const re = /<VirtualHost([^>]*)>([\s\S]*?)<\/VirtualHost>/gi;
|
|
@@ -464,15 +948,44 @@ async function scanApache(executor) {
|
|
|
464
948
|
const text = stripComments(raw);
|
|
465
949
|
const blocks = vhostBlocks(text);
|
|
466
950
|
if (blocks.length === 0) warnings.push("apache: no <VirtualHost> blocks found");
|
|
951
|
+
const redirectOnlyHosts = [];
|
|
467
952
|
for (const { args, body } of blocks) {
|
|
468
953
|
const parsed = parseVhost(body, args);
|
|
954
|
+
if (parsed === null) {
|
|
955
|
+
redirectOnlyHosts.push(redirectVhostNames(body));
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
469
958
|
if ("warning" in parsed) warnings.push(parsed.warning);
|
|
470
959
|
else sites.push(parsed);
|
|
471
960
|
}
|
|
472
|
-
|
|
961
|
+
const served = new Set(sites.flatMap((s) => s.serverNames));
|
|
962
|
+
for (const names of redirectOnlyHosts) {
|
|
963
|
+
const orphaned = names.filter((n) => !served.has(n));
|
|
964
|
+
if (orphaned.length > 0) {
|
|
965
|
+
warnings.push(
|
|
966
|
+
`apache: ${orphaned.join(", ")} only redirected to HTTPS and has no TLS VirtualHost here \u2014 nothing to serve it after migration`
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
const { kept, dropped } = collapseByHost(
|
|
971
|
+
sites,
|
|
972
|
+
(s) => s.serverNames,
|
|
973
|
+
(s) => (s.ssl ? 2 : 0) + (s.target.kind === "proxy" ? 1 : 0)
|
|
974
|
+
);
|
|
975
|
+
for (const d of dropped) {
|
|
976
|
+
warnings.push(
|
|
977
|
+
`apache: ${d.serverNames.join(", ")} had a second VirtualHost (${d.ssl ? "TLS" : "plain HTTP"}, ${d.target.kind}) \u2014 kept the TLS/proxy one`
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
return { proxy: "apache", sites: kept, warnings };
|
|
473
981
|
}
|
|
474
982
|
|
|
475
983
|
// ../../packages/adapters/src/system/proxy/import/traefik.ts
|
|
984
|
+
function lowerKeys(labels) {
|
|
985
|
+
const out = {};
|
|
986
|
+
for (const [k, v] of Object.entries(labels)) out[k.toLowerCase()] = v;
|
|
987
|
+
return out;
|
|
988
|
+
}
|
|
476
989
|
function extractHosts(rule) {
|
|
477
990
|
const hosts = [];
|
|
478
991
|
for (const call of rule.matchAll(/\bHost\(([^)]*)\)/gi)) {
|
|
@@ -481,7 +994,7 @@ function extractHosts(rule) {
|
|
|
481
994
|
if (h) hosts.push(h);
|
|
482
995
|
}
|
|
483
996
|
}
|
|
484
|
-
return hosts;
|
|
997
|
+
return [...new Set(hosts)];
|
|
485
998
|
}
|
|
486
999
|
function extraMatchers(rule) {
|
|
487
1000
|
const found = /* @__PURE__ */ new Set();
|
|
@@ -492,23 +1005,59 @@ function extraMatchers(rule) {
|
|
|
492
1005
|
}
|
|
493
1006
|
return [...found];
|
|
494
1007
|
}
|
|
1008
|
+
function collectDefinitions(containers) {
|
|
1009
|
+
const services = /* @__PURE__ */ new Map();
|
|
1010
|
+
const redirectMiddlewares = /* @__PURE__ */ new Set();
|
|
1011
|
+
const streamRouters = [];
|
|
1012
|
+
let fileProvider = false;
|
|
1013
|
+
let exposedByDefault = true;
|
|
1014
|
+
for (const c of containers) {
|
|
1015
|
+
const labels = lowerKeys(c.labels);
|
|
1016
|
+
for (const [k, v] of Object.entries(labels)) {
|
|
1017
|
+
let m = k.match(/^traefik\.http\.services\.([^.]+)\.loadbalancer\.server\.(port|scheme)$/);
|
|
1018
|
+
if (m) {
|
|
1019
|
+
const def = services.get(m[1]) ?? {};
|
|
1020
|
+
if (m[2] === "port") def.port = v;
|
|
1021
|
+
else def.scheme = v;
|
|
1022
|
+
services.set(m[1], def);
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
m = k.match(/^traefik\.http\.middlewares\.([^.]+)\.redirectscheme\./);
|
|
1026
|
+
if (m) {
|
|
1027
|
+
redirectMiddlewares.add(m[1]);
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
m = k.match(/^traefik\.(tcp|udp)\.routers\.([^.]+)\.rule$/);
|
|
1031
|
+
if (m) streamRouters.push(`${m[1]} router "${m[2]}"`);
|
|
1032
|
+
}
|
|
1033
|
+
const cmd = c.cmd ?? "";
|
|
1034
|
+
if (/--?providers\.file/i.test(cmd)) fileProvider = true;
|
|
1035
|
+
if (/--?providers\.docker\.exposedbydefault[= ]?false/i.test(cmd)) exposedByDefault = false;
|
|
1036
|
+
}
|
|
1037
|
+
return { services, redirectMiddlewares, streamRouters, fileProvider, exposedByDefault };
|
|
1038
|
+
}
|
|
1039
|
+
function resolveIp(c, labels) {
|
|
1040
|
+
const wanted = labels["traefik.docker.network"];
|
|
1041
|
+
if (wanted && c.networks?.[wanted]) return c.networks[wanted];
|
|
1042
|
+
if (c.ip) return c.ip;
|
|
1043
|
+
const first = Object.values(c.networks ?? {}).find(Boolean);
|
|
1044
|
+
return first || void 0;
|
|
1045
|
+
}
|
|
495
1046
|
function parseTraefikLabels(containers) {
|
|
496
|
-
const sites = [];
|
|
497
1047
|
const warnings = [];
|
|
1048
|
+
const defs = collectDefinitions(containers);
|
|
1049
|
+
const candidates = [];
|
|
498
1050
|
for (const c of containers) {
|
|
499
|
-
|
|
1051
|
+
const labels = lowerKeys(c.labels);
|
|
1052
|
+
if (labels["traefik.enable"] === "false") continue;
|
|
1053
|
+
if (!defs.exposedByDefault && labels["traefik.enable"] !== "true") continue;
|
|
500
1054
|
const routers = /* @__PURE__ */ new Map();
|
|
501
|
-
const
|
|
502
|
-
for (const [key, val] of Object.entries(c.labels)) {
|
|
1055
|
+
for (const [key, val] of Object.entries(labels)) {
|
|
503
1056
|
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);
|
|
1057
|
+
if (!r) continue;
|
|
1058
|
+
const bag = routers.get(r[1]) ?? {};
|
|
1059
|
+
bag[r[2]] = val;
|
|
1060
|
+
routers.set(r[1], bag);
|
|
512
1061
|
}
|
|
513
1062
|
for (const [rname, props] of routers) {
|
|
514
1063
|
const rule = props.rule;
|
|
@@ -520,47 +1069,92 @@ function parseTraefikLabels(containers) {
|
|
|
520
1069
|
);
|
|
521
1070
|
continue;
|
|
522
1071
|
}
|
|
523
|
-
|
|
1072
|
+
const ip = resolveIp(c, labels);
|
|
1073
|
+
if (!ip) {
|
|
524
1074
|
warnings.push(
|
|
525
1075
|
`traefik: ${hosts.join(", ")} \u2014 couldn't resolve container ${c.name}'s IP; re-add the upstream manually`
|
|
526
1076
|
);
|
|
527
|
-
continue;
|
|
528
|
-
}
|
|
529
|
-
const svc = props.service;
|
|
530
|
-
const rawPort = (svc && servicePorts.get(svc)) ?? (servicePorts.size === 1 ? [...servicePorts.values()][0] : void 0) ?? "80";
|
|
531
|
-
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`);
|
|
1077
|
+
continue;
|
|
541
1078
|
}
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
1079
|
+
const svc = props.service?.replace(/@.*$/, "");
|
|
1080
|
+
const def = (svc ? defs.services.get(svc) : void 0) ?? (defs.services.size === 1 ? [...defs.services.values()][0] : void 0);
|
|
1081
|
+
const onlyExposed = c.exposedPorts?.length === 1 ? c.exposedPorts[0].split("/")[0] : void 0;
|
|
1082
|
+
const rawPort = def?.port ?? onlyExposed;
|
|
1083
|
+
const port = /^\d{1,5}$/.test(String(rawPort)) ? String(rawPort) : "80";
|
|
1084
|
+
const scheme = def?.scheme?.toLowerCase() === "https" ? "https" : "http";
|
|
1085
|
+
const middlewares = (props.middlewares ?? "").split(",").map((m) => m.trim().replace(/@.*$/, "")).filter(Boolean);
|
|
1086
|
+
candidates.push({
|
|
1087
|
+
hosts,
|
|
1088
|
+
ssl: Object.keys(props).some((p) => p === "tls" || p.startsWith("tls.")),
|
|
1089
|
+
url: `${scheme}://${ip}:${port}`,
|
|
1090
|
+
container: c.name,
|
|
1091
|
+
router: rname,
|
|
1092
|
+
rule,
|
|
1093
|
+
middlewares,
|
|
1094
|
+
redirectOnly: middlewares.length > 0 && middlewares.every((m) => defs.redirectMiddlewares.has(m)),
|
|
1095
|
+
portGuessed: rawPort === void 0
|
|
548
1096
|
});
|
|
549
1097
|
}
|
|
550
1098
|
}
|
|
551
|
-
|
|
1099
|
+
const { kept: sites, dropped } = collapseByHost(
|
|
1100
|
+
candidates,
|
|
1101
|
+
(c) => c.hosts,
|
|
1102
|
+
(c) => c.redirectOnly ? 0 : c.ssl ? 2 : 1
|
|
1103
|
+
);
|
|
1104
|
+
for (const cand of sites) {
|
|
1105
|
+
const extras = extraMatchers(cand.rule);
|
|
1106
|
+
if (extras.length > 0) {
|
|
1107
|
+
warnings.push(
|
|
1108
|
+
`traefik: ${cand.hosts.join(", ")} also matches ${extras.join(", ")} \u2014 only the Host() part is migrated; re-add path/header rules manually`
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
const carried = cand.middlewares.filter((m) => !defs.redirectMiddlewares.has(m));
|
|
1112
|
+
if (carried.length > 0) {
|
|
1113
|
+
warnings.push(
|
|
1114
|
+
`traefik: ${cand.hosts.join(", ")} uses middleware(s) "${carried.join(", ")}" \u2014 not migrated`
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
if (cand.portGuessed) {
|
|
1118
|
+
warnings.push(
|
|
1119
|
+
`traefik: ${cand.hosts.join(", ")} \u2014 no service port label and no single exposed port on ${cand.container}; assumed :80, check the upstream`
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
for (const d of dropped) {
|
|
1124
|
+
warnings.push(
|
|
1125
|
+
`traefik: ${d.hosts.join(", ")} router "${d.router}" is an http\u2192https redirect \u2014 Openship's edge does that itself (nothing to migrate)`
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
for (const s of defs.streamRouters) {
|
|
1129
|
+
warnings.push(`traefik: ${s} can't migrate \u2014 Openship's edge routes HTTP(S), not raw TCP/UDP`);
|
|
1130
|
+
}
|
|
1131
|
+
if (defs.fileProvider) {
|
|
1132
|
+
warnings.push(
|
|
1133
|
+
"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."
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
return {
|
|
1137
|
+
proxy: "traefik",
|
|
1138
|
+
sites: sites.map((cand) => ({
|
|
1139
|
+
serverNames: cand.hosts,
|
|
1140
|
+
ssl: cand.ssl,
|
|
1141
|
+
target: { kind: "proxy", url: cand.url },
|
|
1142
|
+
source: `traefik container ${cand.container}`
|
|
1143
|
+
})),
|
|
1144
|
+
warnings
|
|
1145
|
+
};
|
|
552
1146
|
}
|
|
553
1147
|
async function scanTraefik(executor) {
|
|
554
|
-
const out = await
|
|
1148
|
+
const out = await tryExec2(
|
|
555
1149
|
executor,
|
|
556
|
-
|
|
1150
|
+
`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
1151
|
);
|
|
558
1152
|
if (!out) {
|
|
559
1153
|
return { proxy: "traefik", sites: [], warnings: ["traefik: couldn't read container labels via docker inspect"] };
|
|
560
1154
|
}
|
|
561
1155
|
const containers = [];
|
|
562
1156
|
for (const line of out.split("\n")) {
|
|
563
|
-
const [rawName, rawLabels,
|
|
1157
|
+
const [rawName, rawLabels, rawNets, rawPorts, rawCmd] = line.split(" ");
|
|
564
1158
|
if (!rawName || !rawLabels) continue;
|
|
565
1159
|
let labels;
|
|
566
1160
|
try {
|
|
@@ -568,9 +1162,30 @@ async function scanTraefik(executor) {
|
|
|
568
1162
|
} catch {
|
|
569
1163
|
continue;
|
|
570
1164
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
1165
|
+
const networks = {};
|
|
1166
|
+
let bareIp;
|
|
1167
|
+
for (const pair of (rawNets ?? "").trim().split(/\s+/)) {
|
|
1168
|
+
if (!pair) continue;
|
|
1169
|
+
const eq = pair.indexOf("=");
|
|
1170
|
+
if (eq > 0) {
|
|
1171
|
+
const ip = pair.slice(eq + 1);
|
|
1172
|
+
if (ip) networks[pair.slice(0, eq)] = ip;
|
|
1173
|
+
} else if (!bareIp) {
|
|
1174
|
+
bareIp = pair;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
const exposedPorts = (rawPorts ?? "").trim().split(/\s+/).filter(Boolean);
|
|
1178
|
+
const hasTraefikLabels = Object.keys(labels).some((k) => k.toLowerCase().startsWith("traefik."));
|
|
1179
|
+
const looksLikeTraefik = /traefik/i.test(rawCmd ?? "") || /--?providers\./i.test(rawCmd ?? "");
|
|
1180
|
+
if (!hasTraefikLabels && !looksLikeTraefik) continue;
|
|
1181
|
+
containers.push({
|
|
1182
|
+
name: rawName.replace(/^\//, ""),
|
|
1183
|
+
labels,
|
|
1184
|
+
ip: Object.values(networks)[0] ?? bareIp,
|
|
1185
|
+
networks,
|
|
1186
|
+
exposedPorts,
|
|
1187
|
+
cmd: rawCmd ?? void 0
|
|
1188
|
+
});
|
|
574
1189
|
}
|
|
575
1190
|
return parseTraefikLabels(containers);
|
|
576
1191
|
}
|
|
@@ -612,7 +1227,7 @@ async function scanImportableSites(executor, proxy) {
|
|
|
612
1227
|
case "traefik":
|
|
613
1228
|
return scanTraefik(executor);
|
|
614
1229
|
case "openresty":
|
|
615
|
-
return
|
|
1230
|
+
return scanForeignOpenResty(executor);
|
|
616
1231
|
default:
|
|
617
1232
|
return {
|
|
618
1233
|
proxy,
|
|
@@ -624,6 +1239,27 @@ async function scanImportableSites(executor, proxy) {
|
|
|
624
1239
|
function canImportProxy(proxy) {
|
|
625
1240
|
return proxy === "nginx" || proxy === "caddy" || proxy === "apache" || proxy === "traefik" || proxy === "openresty";
|
|
626
1241
|
}
|
|
1242
|
+
function unreachableStaticRoots(sites, opts) {
|
|
1243
|
+
if (!opts.containerEdge) return [];
|
|
1244
|
+
const visible = EDGE_CONTAINER_MOUNTS.map((m) => m.host.replace(/\/+$/, ""));
|
|
1245
|
+
const out = [];
|
|
1246
|
+
for (const site of sites) {
|
|
1247
|
+
if (site.target.kind !== "static") continue;
|
|
1248
|
+
const root = site.target.root.replace(/\/+$/, "");
|
|
1249
|
+
const reachable = visible.some((v) => root === v || root.startsWith(`${v}/`));
|
|
1250
|
+
if (!reachable) out.push({ host: site.serverNames[0] ?? root, root: site.target.root });
|
|
1251
|
+
}
|
|
1252
|
+
return out;
|
|
1253
|
+
}
|
|
1254
|
+
var EDGE_STATIC_MOUNT = EDGE_CONTAINER_MOUNTS.find((m) => m.host === "/opt/openship/static")?.host ?? "/opt/openship/static";
|
|
1255
|
+
async function copyStaticRootIntoEdge(executor, opts) {
|
|
1256
|
+
const slug = opts.host.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "site";
|
|
1257
|
+
const src = opts.root.replace(/\/+$/, "");
|
|
1258
|
+
const dest = `${EDGE_STATIC_MOUNT}/_adopted/${slug}`;
|
|
1259
|
+
await executor.exec(`mkdir -p ${sq2(dest)}`);
|
|
1260
|
+
await executor.exec(`cp -a ${sq2(`${src}/.`)} ${sq2(`${dest}/`)}`);
|
|
1261
|
+
return dest;
|
|
1262
|
+
}
|
|
627
1263
|
|
|
628
1264
|
// ../../packages/adapters/src/system/proxy/import/caddy-certs.ts
|
|
629
1265
|
var CADDY_DATA_ROOTS = [
|
|
@@ -641,10 +1277,10 @@ function candidateNames(host) {
|
|
|
641
1277
|
async function findPairIn(exec, root, host, container) {
|
|
642
1278
|
for (const name of candidateNames(host)) {
|
|
643
1279
|
const glob = `${root}/certificates/*/${name}/${name}.crt`;
|
|
644
|
-
let certPath = firstCrt(await
|
|
1280
|
+
let certPath = firstCrt(await tryExec2(exec, `ls -1 ${glob} 2>/dev/null`));
|
|
645
1281
|
if (!certPath && container) {
|
|
646
1282
|
certPath = firstCrt(
|
|
647
|
-
await
|
|
1283
|
+
await tryExec2(exec, containerCommand(container, `ls -1 ${glob} 2>/dev/null`))
|
|
648
1284
|
);
|
|
649
1285
|
}
|
|
650
1286
|
if (certPath) return { certPath, keyPath: certPath.replace(/\.crt$/, ".key") };
|
|
@@ -690,7 +1326,7 @@ async function candidateStoragePaths(exec, container) {
|
|
|
690
1326
|
};
|
|
691
1327
|
if (container) {
|
|
692
1328
|
collect(
|
|
693
|
-
await
|
|
1329
|
+
await tryExec2(
|
|
694
1330
|
exec,
|
|
695
1331
|
`docker inspect ${sq2(container)} --format '{{json .Config.Cmd}} {{json .Args}} {{json .Config.Env}}' 2>/dev/null`
|
|
696
1332
|
)
|
|
@@ -794,11 +1430,13 @@ function buildProxyRouteIndex(sites) {
|
|
|
794
1430
|
existing.ssl.keyPath = site.tls.keyPath;
|
|
795
1431
|
}
|
|
796
1432
|
}
|
|
1433
|
+
if (site.proxy && (site.ssl || !existing.proxy)) existing.proxy = site.proxy;
|
|
797
1434
|
} else {
|
|
798
1435
|
list.push({
|
|
799
1436
|
port,
|
|
800
1437
|
path: up.path,
|
|
801
1438
|
domains: [...site.serverNames],
|
|
1439
|
+
...site.proxy ? { proxy: site.proxy } : {},
|
|
802
1440
|
ssl: site.ssl ? { enabled: true, certPath: site.tls?.certPath, keyPath: site.tls?.keyPath } : { enabled: false },
|
|
803
1441
|
source: site.source
|
|
804
1442
|
});
|
|
@@ -855,7 +1493,7 @@ async function edgeProxy(exec, opts = {}) {
|
|
|
855
1493
|
return makeApi(exec, kind, ours, container);
|
|
856
1494
|
}
|
|
857
1495
|
function edgeProxyFor(exec, kind, opts = {}) {
|
|
858
|
-
return makeApi(exec, kind, opts.ours ??
|
|
1496
|
+
return makeApi(exec, kind, opts.ours ?? false, opts.container ?? null);
|
|
859
1497
|
}
|
|
860
1498
|
async function collectProxyCerts(api, sites) {
|
|
861
1499
|
const certPems = {};
|
|
@@ -919,147 +1557,6 @@ function makeApi(exec, kind, ours, container) {
|
|
|
919
1557
|
};
|
|
920
1558
|
}
|
|
921
1559
|
|
|
922
|
-
// ../../packages/adapters/src/system/proxy/takeover-journal.ts
|
|
923
|
-
var JOURNAL_DIR = "/var/lib/openship";
|
|
924
|
-
var JOURNAL_PATH = `${JOURNAL_DIR}/edge-takeover.json`;
|
|
925
|
-
function log(message, level = "info") {
|
|
926
|
-
return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
|
|
927
|
-
}
|
|
928
|
-
async function tryExec2(executor, cmd) {
|
|
929
|
-
try {
|
|
930
|
-
return await executor.exec(cmd);
|
|
931
|
-
} catch {
|
|
932
|
-
return null;
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
async function buildJournal(executor, status) {
|
|
936
|
-
const units = /* @__PURE__ */ new Map();
|
|
937
|
-
const containers = /* @__PURE__ */ new Map();
|
|
938
|
-
const processes = /* @__PURE__ */ new Map();
|
|
939
|
-
for (const o of status.occupants) {
|
|
940
|
-
if (o.containerName && !containers.has(o.containerName)) {
|
|
941
|
-
const r = await tryExec2(
|
|
942
|
-
executor,
|
|
943
|
-
`docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' ${sq2(o.containerName)} 2>/dev/null`
|
|
944
|
-
);
|
|
945
|
-
containers.set(o.containerName, { name: o.containerName, restart: r?.trim() || "no" });
|
|
946
|
-
} else if (o.systemdUnit && !units.has(o.systemdUnit)) {
|
|
947
|
-
const en = await tryExec2(executor, `systemctl is-enabled ${sq2(o.systemdUnit)} 2>/dev/null`);
|
|
948
|
-
units.set(o.systemdUnit, { unit: o.systemdUnit, wasEnabled: en?.trim() === "enabled" });
|
|
949
|
-
} else if (!o.containerName && !o.systemdUnit && o.pid && !processes.has(o.pid)) {
|
|
950
|
-
processes.set(o.pid, { pid: o.pid, command: o.rawCommand });
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
return {
|
|
954
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
955
|
-
units: [...units.values()],
|
|
956
|
-
containers: [...containers.values()],
|
|
957
|
-
processes: [...processes.values()]
|
|
958
|
-
};
|
|
959
|
-
}
|
|
960
|
-
async function writeJournal(executor, journal) {
|
|
961
|
-
try {
|
|
962
|
-
await executor.mkdir(JOURNAL_DIR);
|
|
963
|
-
await executor.writeFile(JOURNAL_PATH, JSON.stringify(journal, null, 2));
|
|
964
|
-
} catch {
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
async function clearJournal(executor) {
|
|
968
|
-
await tryExec2(executor, `rm -f ${JOURNAL_PATH}`);
|
|
969
|
-
}
|
|
970
|
-
async function readJournal(executor) {
|
|
971
|
-
const raw = await tryExec2(executor, `cat ${JOURNAL_PATH} 2>/dev/null`);
|
|
972
|
-
if (!raw?.trim()) return null;
|
|
973
|
-
try {
|
|
974
|
-
return JSON.parse(raw);
|
|
975
|
-
} catch {
|
|
976
|
-
await clearJournal(executor);
|
|
977
|
-
return null;
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
async function rollback(executor, journal, onLog) {
|
|
981
|
-
onLog(log("Rolling back \u2014 restoring the previous proxy...", "warn"));
|
|
982
|
-
await tryExec2(executor, `docker update --restart=no ${sq2(EDGE_CONTAINER_NAME)} 2>/dev/null || true`);
|
|
983
|
-
await tryExec2(executor, `docker stop ${sq2(EDGE_CONTAINER_NAME)} 2>/dev/null || true`);
|
|
984
|
-
await tryExec2(
|
|
985
|
-
executor,
|
|
986
|
-
"systemctl disable --now openresty 2>/dev/null || systemctl stop openresty 2>/dev/null || true; systemctl reset-failed openresty 2>/dev/null || true"
|
|
987
|
-
);
|
|
988
|
-
for (const u of journal.units) {
|
|
989
|
-
await tryExec2(
|
|
990
|
-
executor,
|
|
991
|
-
u.wasEnabled ? `systemctl enable --now ${sq2(u.unit)} 2>/dev/null || true` : `systemctl start ${sq2(u.unit)} 2>/dev/null || true`
|
|
992
|
-
);
|
|
993
|
-
}
|
|
994
|
-
for (const c of journal.containers) {
|
|
995
|
-
await tryExec2(executor, `docker update --restart=${sq2(c.restart)} ${sq2(c.name)} 2>/dev/null || true`);
|
|
996
|
-
await tryExec2(executor, `docker start ${sq2(c.name)} 2>/dev/null || true`);
|
|
997
|
-
}
|
|
998
|
-
for (const p of journal.processes ?? []) {
|
|
999
|
-
if (p.command) {
|
|
1000
|
-
await tryExec2(
|
|
1001
|
-
executor,
|
|
1002
|
-
`setsid -f sh -c ${sq2(p.command)} 2>/dev/null || (nohup sh -c ${sq2(p.command)} >/dev/null 2>&1 &) || true`
|
|
1003
|
-
);
|
|
1004
|
-
} else {
|
|
1005
|
-
onLog(log(`Could not restore process ${p.pid} \u2014 no command captured.`, "warn"));
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
const restored = await portIsServed(executor, 80);
|
|
1009
|
-
if (!restored) {
|
|
1010
|
-
onLog(
|
|
1011
|
-
log(
|
|
1012
|
-
"Nothing is listening on :80 after the restore \u2014 the box is NOT serving. Start your proxy by hand (e.g. `systemctl start nginx`, or `docker start <name>`).",
|
|
1013
|
-
"error"
|
|
1014
|
-
)
|
|
1015
|
-
);
|
|
1016
|
-
}
|
|
1017
|
-
return restored;
|
|
1018
|
-
}
|
|
1019
|
-
async function portIsServed(executor, port) {
|
|
1020
|
-
const hex = port.toString(16).toUpperCase().padStart(4, "0");
|
|
1021
|
-
const out = await tryExec2(
|
|
1022
|
-
executor,
|
|
1023
|
-
`(command -v ss >/dev/null 2>&1 && ss -ltn 2>/dev/null | grep -qE ':${port}[[:space:]]' && echo yes) || (grep -qiE '^[[:space:]]*[0-9]+:[[:space:]]*[0-9A-F]{8}:${hex}[[:space:]]+[0-9A-F]{8}:0000[[:space:]]+0A' /proc/net/tcp 2>/dev/null && echo yes) || true`
|
|
1024
|
-
);
|
|
1025
|
-
return (out ?? "").includes("yes");
|
|
1026
|
-
}
|
|
1027
|
-
async function beginEdgeTakeover(executor, status, onLog) {
|
|
1028
|
-
await writeJournal(executor, await buildJournal(executor, status));
|
|
1029
|
-
await freeEdgeTargets(executor, stopTargetsForStatus(status), (m, l) => onLog(log(m, l)));
|
|
1030
|
-
}
|
|
1031
|
-
async function rollbackEdgeTakeover(executor, onLog) {
|
|
1032
|
-
const journal = await readJournal(executor);
|
|
1033
|
-
if (!journal || journal.completed) return false;
|
|
1034
|
-
const restored = await rollback(executor, journal, onLog);
|
|
1035
|
-
await clearJournal(executor);
|
|
1036
|
-
return restored;
|
|
1037
|
-
}
|
|
1038
|
-
async function completeEdgeTakeover(executor) {
|
|
1039
|
-
const journal = await readJournal(executor);
|
|
1040
|
-
if (journal) {
|
|
1041
|
-
journal.completed = true;
|
|
1042
|
-
await writeJournal(executor, journal);
|
|
1043
|
-
}
|
|
1044
|
-
await clearJournal(executor);
|
|
1045
|
-
}
|
|
1046
|
-
async function recoverInterruptedTakeover(executor, onLog, isEdgeHealthy) {
|
|
1047
|
-
const journal = await readJournal(executor);
|
|
1048
|
-
if (!journal) return;
|
|
1049
|
-
if (journal.completed) {
|
|
1050
|
-
await clearJournal(executor);
|
|
1051
|
-
return;
|
|
1052
|
-
}
|
|
1053
|
-
if (isEdgeHealthy && await isEdgeHealthy()) {
|
|
1054
|
-
onLog(log("Found an unfinished edge takeover, but our edge is serving \u2014 keeping it.", "warn"));
|
|
1055
|
-
await clearJournal(executor);
|
|
1056
|
-
return;
|
|
1057
|
-
}
|
|
1058
|
-
onLog(log("Found an interrupted edge takeover \u2014 restoring the previous proxy.", "warn"));
|
|
1059
|
-
await rollback(executor, journal, onLog);
|
|
1060
|
-
await clearJournal(executor);
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
1560
|
// ../../packages/adapters/src/system/catalog.ts
|
|
1064
1561
|
function dockerInstallPlan(profile) {
|
|
1065
1562
|
if (profile.os !== "linux") {
|
|
@@ -1117,120 +1614,15 @@ function rsyncInstallPlan(profile) {
|
|
|
1117
1614
|
verifyCommand: "rsync --version | head -n 1"
|
|
1118
1615
|
};
|
|
1119
1616
|
}
|
|
1120
|
-
var OPENRESTY_APT_SOURCES = [
|
|
1121
|
-
// ARM64: openresty.org publishes aarch64 debs under a SEPARATE path
|
|
1122
|
-
// (openresty.org/package/arm64/<repo>); the default path is amd64-only, so on
|
|
1123
|
-
// aarch64 `apt-get install openresty` finds no candidate and `set -e` aborts
|
|
1124
|
-
// the whole setup (issue #109). Switch the base URL by arch and tag the deb
|
|
1125
|
-
// line with `arch=` so apt fetches the right index. `dpkg --print-architecture`
|
|
1126
|
-
// returns exactly amd64/arm64 (matches OpenResty's path + arch qualifier).
|
|
1127
|
-
'OR_ARCH="$(dpkg --print-architecture 2>/dev/null || echo amd64)"',
|
|
1128
|
-
'if [ "$OR_ARCH" = arm64 ]; then OR_BASE="http://openresty.org/package/arm64/$REPO"; else OR_BASE="http://openresty.org/package/$REPO"; fi',
|
|
1129
|
-
// `|| REPO_CODENAME=""` so a failed substitution (no lsb_release AND no
|
|
1130
|
-
// /etc/os-release, e.g. a stripped container) doesn't trip `set -e` before
|
|
1131
|
-
// the empty-var fallback below can pick a default.
|
|
1132
|
-
'REPO_CODENAME="$(lsb_release -sc 2>/dev/null || { . /etc/os-release 2>/dev/null && echo "$VERSION_CODENAME"; })" || REPO_CODENAME=""',
|
|
1133
|
-
'if [ "$REPO" = debian ]; then OR_FALLBACKS="bookworm bullseye"; else OR_FALLBACKS="noble jammy focal"; fi',
|
|
1134
|
-
'OR_CODENAME=""',
|
|
1135
|
-
"for c in $REPO_CODENAME $OR_FALLBACKS; do",
|
|
1136
|
-
' if wget -q --spider --tries=2 --timeout=15 "$OR_BASE/dists/$c/Release"; then OR_CODENAME="$c"; break; fi',
|
|
1137
|
-
"done",
|
|
1138
|
-
'if [ -z "$OR_CODENAME" ]; then case "$REPO" in debian) OR_CODENAME=bookworm ;; *) OR_CODENAME=noble ;; esac; fi',
|
|
1139
|
-
'[ "$OR_CODENAME" = "$REPO_CODENAME" ] || echo "[openresty] apt repo has no codename $REPO_CODENAME; using nearest supported LTS $OR_CODENAME" >&2',
|
|
1140
|
-
'echo "deb [arch=$OR_ARCH signed-by=/usr/share/keyrings/openresty.gpg] $OR_BASE $OR_CODENAME main" > /etc/apt/sources.list.d/openresty.list'
|
|
1141
|
-
];
|
|
1142
|
-
function openrestyInstallPlan(profile) {
|
|
1143
|
-
if (profile.os !== "linux") {
|
|
1144
|
-
return {
|
|
1145
|
-
supported: false,
|
|
1146
|
-
unsupportedReason: "OpenResty installation is only supported on Linux servers"
|
|
1147
|
-
};
|
|
1148
|
-
}
|
|
1149
|
-
let installCommand;
|
|
1150
|
-
if (profile.packageManager === "apt") {
|
|
1151
|
-
const distro = profile.distro === "debian" ? "debian" : "ubuntu";
|
|
1152
|
-
installCommand = [
|
|
1153
|
-
"set -e",
|
|
1154
|
-
// Heal a stale/bad openresty.list left by a prior run (e.g. the #86
|
|
1155
|
-
// 'resolute' pin) BEFORE the first apt-get update reads it — otherwise
|
|
1156
|
-
// set -e aborts here and the repo never gets rewritten.
|
|
1157
|
-
"rm -f /etc/apt/sources.list.d/openresty.list",
|
|
1158
|
-
"apt-get update -qq && apt-get install -y -qq wget gnupg2 lsb-release",
|
|
1159
|
-
"wget -qO /tmp/openresty-pubkey.gpg https://openresty.org/package/pubkey.gpg",
|
|
1160
|
-
"gpg --yes --dearmor -o /usr/share/keyrings/openresty.gpg /tmp/openresty-pubkey.gpg",
|
|
1161
|
-
`REPO=${distro}`,
|
|
1162
|
-
...OPENRESTY_APT_SOURCES,
|
|
1163
|
-
"apt-get update -qq && apt-get install -y -qq openresty"
|
|
1164
|
-
].join("\n");
|
|
1165
|
-
} else if (profile.packageManager === "dnf") {
|
|
1166
|
-
const distro = profile.distro === "fedora" ? "fedora" : "centos";
|
|
1167
|
-
installCommand = `wget -qO /etc/yum.repos.d/openresty.repo https://openresty.org/package/${distro}/openresty.repo && dnf install -y openresty`;
|
|
1168
|
-
} else if (profile.packageManager === "yum") {
|
|
1169
|
-
installCommand = "wget -qO /etc/yum.repos.d/openresty.repo https://openresty.org/package/centos/openresty.repo && yum install -y openresty";
|
|
1170
|
-
} else if (profile.packageManager === "apk") {
|
|
1171
|
-
installCommand = [
|
|
1172
|
-
"apk add --no-cache wget",
|
|
1173
|
-
"wget -qO /etc/apk/keys/admin@openresty.com-5ea678a6.rsa.pub https://openresty.org/package/alpine/admin@openresty.com-5ea678a6.rsa.pub",
|
|
1174
|
-
`. /etc/os-release && echo "https://openresty.org/package/alpine/v$( echo $VERSION_ID | cut -d. -f1,2 )/main" >> /etc/apk/repositories`,
|
|
1175
|
-
"apk update && apk add openresty"
|
|
1176
|
-
].join(" && ");
|
|
1177
|
-
} else {
|
|
1178
|
-
installCommand = [
|
|
1179
|
-
"set -e",
|
|
1180
|
-
"if command -v apt-get >/dev/null 2>&1; then",
|
|
1181
|
-
" rm -f /etc/apt/sources.list.d/openresty.list",
|
|
1182
|
-
" apt-get update -qq && apt-get install -y -qq wget gnupg2 lsb-release",
|
|
1183
|
-
" wget -qO /tmp/openresty-pubkey.gpg https://openresty.org/package/pubkey.gpg",
|
|
1184
|
-
" gpg --yes --dearmor -o /usr/share/keyrings/openresty.gpg /tmp/openresty-pubkey.gpg",
|
|
1185
|
-
' DISTRO=$(. /etc/os-release 2>/dev/null && echo "$ID" || echo "ubuntu")',
|
|
1186
|
-
' case "$DISTRO" in debian) REPO=debian ;; *) REPO=ubuntu ;; esac',
|
|
1187
|
-
...OPENRESTY_APT_SOURCES.map((l) => " " + l),
|
|
1188
|
-
" apt-get update -qq && apt-get install -y -qq openresty",
|
|
1189
|
-
"elif command -v dnf >/dev/null 2>&1; then",
|
|
1190
|
-
" wget -qO /etc/yum.repos.d/openresty.repo https://openresty.org/package/centos/openresty.repo && dnf install -y openresty",
|
|
1191
|
-
"elif command -v yum >/dev/null 2>&1; then",
|
|
1192
|
-
" wget -qO /etc/yum.repos.d/openresty.repo https://openresty.org/package/centos/openresty.repo && yum install -y openresty",
|
|
1193
|
-
"elif command -v apk >/dev/null 2>&1; then",
|
|
1194
|
-
" apk add --no-cache wget \\",
|
|
1195
|
-
" && wget -qO /etc/apk/keys/admin@openresty.com-5ea678a6.rsa.pub https://openresty.org/package/alpine/admin@openresty.com-5ea678a6.rsa.pub \\",
|
|
1196
|
-
' && . /etc/os-release && echo "https://openresty.org/package/alpine/v$(echo $VERSION_ID | cut -d. -f1,2)/main" >> /etc/apk/repositories \\',
|
|
1197
|
-
" && apk update && apk add openresty",
|
|
1198
|
-
"else",
|
|
1199
|
-
' echo "No supported package manager found (tried apt-get, dnf, yum, apk)" >&2 && exit 1',
|
|
1200
|
-
"fi"
|
|
1201
|
-
].join("\n");
|
|
1202
|
-
}
|
|
1203
|
-
return {
|
|
1204
|
-
supported: true,
|
|
1205
|
-
installCommand,
|
|
1206
|
-
startCommand: profile.serviceManager === "systemd" ? "systemctl enable openresty && systemctl start openresty" : void 0,
|
|
1207
|
-
verifyCommand: "openresty -v 2>&1"
|
|
1208
|
-
};
|
|
1209
|
-
}
|
|
1210
|
-
function certbotInstallPlan(profile) {
|
|
1211
|
-
const commands = {
|
|
1212
|
-
apt: "apt-get update -qq && apt-get install -y -qq certbot",
|
|
1213
|
-
dnf: "dnf install -y certbot",
|
|
1214
|
-
yum: "yum install -y certbot",
|
|
1215
|
-
apk: "apk add --no-cache certbot"
|
|
1216
|
-
};
|
|
1217
|
-
const installCommand = commands[profile.packageManager];
|
|
1218
|
-
if (!installCommand) {
|
|
1219
|
-
return {
|
|
1220
|
-
supported: false,
|
|
1221
|
-
unsupportedReason: "Certbot installation is only supported on Linux with apt, dnf, or yum"
|
|
1222
|
-
};
|
|
1223
|
-
}
|
|
1224
|
-
return {
|
|
1225
|
-
supported: true,
|
|
1226
|
-
installCommand,
|
|
1227
|
-
verifyCommand: "certbot --version 2>/dev/null"
|
|
1228
|
-
};
|
|
1229
|
-
}
|
|
1230
1617
|
var systemCatalog = {
|
|
1231
1618
|
checks: {
|
|
1232
1619
|
docker: {
|
|
1233
1620
|
versionCommand: "docker --version",
|
|
1621
|
+
// A POSITIVE signal, not a bare exit code: only a daemon that answered can
|
|
1622
|
+
// name its version. `docker info >/dev/null && echo ok` prints ok on ANY
|
|
1623
|
+
// exit-0, so a daemon answering nothing would read as healthy — trading #408's
|
|
1624
|
+
// false negative for a false positive. Leave stderr alone too; `checkDocker`
|
|
1625
|
+
// puts the failure text in the component message.
|
|
1234
1626
|
daemonCommand: "docker info --format '{{.ServerVersion}}'",
|
|
1235
1627
|
parseVersion: (output) => output.match(/Docker version ([^\s,]+)/)?.[1] ?? output,
|
|
1236
1628
|
missingMessage: "Docker is not installed",
|
|
@@ -1261,12 +1653,15 @@ var systemCatalog = {
|
|
|
1261
1653
|
missingMessage: "rsync is not installed"
|
|
1262
1654
|
}
|
|
1263
1655
|
},
|
|
1656
|
+
// No `openresty` / `certbot` plans: nothing apt-installs an edge any more. Both
|
|
1657
|
+
// shipped inside the `openship-edge` image, and their host-package plans (with the
|
|
1658
|
+
// openresty.org apt-repo codename probing that came with them) went with the
|
|
1659
|
+
// installers. `checks.openresty` stays — it parses the version out of the
|
|
1660
|
+
// CONTAINER. See installer.ts `installContainerEdge`.
|
|
1264
1661
|
installs: {
|
|
1265
1662
|
docker: dockerInstallPlan,
|
|
1266
1663
|
git: gitInstallPlan,
|
|
1267
|
-
rsync: rsyncInstallPlan
|
|
1268
|
-
openresty: openrestyInstallPlan,
|
|
1269
|
-
certbot: certbotInstallPlan
|
|
1664
|
+
rsync: rsyncInstallPlan
|
|
1270
1665
|
}
|
|
1271
1666
|
};
|
|
1272
1667
|
|
|
@@ -1290,7 +1685,7 @@ function elevatedExecutor(inner) {
|
|
|
1290
1685
|
};
|
|
1291
1686
|
const overrides = {
|
|
1292
1687
|
exec: (command, opts) => inner.exec(elevateCommand(command), opts),
|
|
1293
|
-
streamExec: (command, onLog) => inner.streamExec(elevateCommand(command), onLog),
|
|
1688
|
+
streamExec: (command, onLog, opts) => inner.streamExec(elevateCommand(command), onLog, opts),
|
|
1294
1689
|
writeFile: writeFileElevated,
|
|
1295
1690
|
// Elevated for the same reason as writeFile: the vhost/conf dirs are root-owned,
|
|
1296
1691
|
// and the Proxy would otherwise forward this to the inner executor unelevated —
|
|
@@ -1317,7 +1712,7 @@ function elevatedExecutor(inner) {
|
|
|
1317
1712
|
}
|
|
1318
1713
|
|
|
1319
1714
|
// ../../packages/adapters/src/system/installer.ts
|
|
1320
|
-
function
|
|
1715
|
+
function log3(message, level = "info") {
|
|
1321
1716
|
return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
|
|
1322
1717
|
}
|
|
1323
1718
|
async function prepareExecutor(executor, component) {
|
|
@@ -1342,21 +1737,21 @@ async function installDocker(executor, onLog) {
|
|
|
1342
1737
|
if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
|
|
1343
1738
|
return { component: "docker", success: false, error: plan.unsupportedReason ?? "Docker install not supported" };
|
|
1344
1739
|
}
|
|
1345
|
-
onLog(
|
|
1740
|
+
onLog(log3("Installing Docker Engine..."));
|
|
1346
1741
|
try {
|
|
1347
1742
|
const { code } = await executor.streamExec(plan.installCommand, onLog);
|
|
1348
1743
|
if (code !== 0) return { component: "docker", success: false, error: "Docker install failed" };
|
|
1349
1744
|
if (plan.startCommand) {
|
|
1350
|
-
onLog(
|
|
1745
|
+
onLog(log3("Starting Docker service..."));
|
|
1351
1746
|
await executor.streamExec(plan.startCommand, onLog);
|
|
1352
1747
|
}
|
|
1353
1748
|
const version = await executor.exec(plan.verifyCommand);
|
|
1354
1749
|
const parsed = systemCatalog.checks.docker.parseVersion(version);
|
|
1355
|
-
onLog(
|
|
1750
|
+
onLog(log3(`Docker ${parsed} installed`));
|
|
1356
1751
|
return { component: "docker", success: true, version: parsed };
|
|
1357
1752
|
} catch (err) {
|
|
1358
1753
|
const msg = safeErrorMessage(err);
|
|
1359
|
-
onLog(
|
|
1754
|
+
onLog(log3(`Docker installation failed: ${msg}`, "error"));
|
|
1360
1755
|
return { component: "docker", success: false, error: msg };
|
|
1361
1756
|
}
|
|
1362
1757
|
}
|
|
@@ -1369,17 +1764,17 @@ async function installGit(executor, onLog, opts) {
|
|
|
1369
1764
|
if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
|
|
1370
1765
|
return { component: "git", success: false, error: plan.unsupportedReason ?? "Git install not supported" };
|
|
1371
1766
|
}
|
|
1372
|
-
onLog(
|
|
1767
|
+
onLog(log3(opts?.label ? `Installing Git in ${opts.label}...` : "Installing Git..."));
|
|
1373
1768
|
try {
|
|
1374
1769
|
const { code } = await executor.streamExec(plan.installCommand, onLog);
|
|
1375
1770
|
if (code !== 0) return { component: "git", success: false, error: "Git installation failed" };
|
|
1376
1771
|
const version = await executor.exec(plan.verifyCommand);
|
|
1377
1772
|
const parsed = systemCatalog.checks.git.parseVersion(version);
|
|
1378
|
-
onLog(
|
|
1773
|
+
onLog(log3(opts?.label ? `Git ${parsed} installed in ${opts.label}` : `Git ${parsed} installed`));
|
|
1379
1774
|
return { component: "git", success: true, version: parsed };
|
|
1380
1775
|
} catch (err) {
|
|
1381
1776
|
const msg = safeErrorMessage(err);
|
|
1382
|
-
onLog(
|
|
1777
|
+
onLog(log3(`Git installation failed: ${msg}`, "error"));
|
|
1383
1778
|
return { component: "git", success: false, error: msg };
|
|
1384
1779
|
}
|
|
1385
1780
|
}
|
|
@@ -1392,17 +1787,17 @@ async function installRsync(executor, onLog) {
|
|
|
1392
1787
|
if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
|
|
1393
1788
|
return { component: "rsync", success: false, error: plan.unsupportedReason ?? "rsync install not supported" };
|
|
1394
1789
|
}
|
|
1395
|
-
onLog(
|
|
1790
|
+
onLog(log3("Installing rsync..."));
|
|
1396
1791
|
try {
|
|
1397
1792
|
const { code } = await executor.streamExec(plan.installCommand, onLog);
|
|
1398
1793
|
if (code !== 0) return { component: "rsync", success: false, error: "rsync installation failed" };
|
|
1399
1794
|
const version = await executor.exec(plan.verifyCommand);
|
|
1400
1795
|
const parsed = systemCatalog.checks.rsync.parseVersion(version);
|
|
1401
|
-
onLog(
|
|
1796
|
+
onLog(log3(`rsync ${parsed} installed`));
|
|
1402
1797
|
return { component: "rsync", success: true, version: parsed };
|
|
1403
1798
|
} catch (err) {
|
|
1404
1799
|
const msg = safeErrorMessage(err);
|
|
1405
|
-
onLog(
|
|
1800
|
+
onLog(log3(`rsync installation failed: ${msg}`, "error"));
|
|
1406
1801
|
return { component: "rsync", success: false, error: msg };
|
|
1407
1802
|
}
|
|
1408
1803
|
}
|
|
@@ -1412,7 +1807,7 @@ async function installContainerEdge(executor, onLog, config) {
|
|
|
1412
1807
|
executor = prep.executor;
|
|
1413
1808
|
if (!await dockerAvailable(executor)) {
|
|
1414
1809
|
const error = "The edge is a container image and needs Docker on this server. Install the Docker component first, then install the edge.";
|
|
1415
|
-
onLog(
|
|
1810
|
+
onLog(log3(error, "error"));
|
|
1416
1811
|
return { component: "edge", success: false, error };
|
|
1417
1812
|
}
|
|
1418
1813
|
try {
|
|
@@ -1422,7 +1817,7 @@ async function installContainerEdge(executor, onLog, config) {
|
|
|
1422
1817
|
image: config?.edgeImage
|
|
1423
1818
|
});
|
|
1424
1819
|
if (result.converted) {
|
|
1425
|
-
onLog(
|
|
1820
|
+
onLog(log3("Edge moved from host OpenResty to the container edge", "warn"));
|
|
1426
1821
|
}
|
|
1427
1822
|
const version = await executor.exec(containerCommand(result.container, "openresty -v 2>&1")).catch(() => "");
|
|
1428
1823
|
return {
|
|
@@ -1433,18 +1828,17 @@ async function installContainerEdge(executor, onLog, config) {
|
|
|
1433
1828
|
} catch (err) {
|
|
1434
1829
|
if (err instanceof EdgeMigrateRequested) throw err;
|
|
1435
1830
|
const msg = safeErrorMessage(err);
|
|
1436
|
-
onLog(
|
|
1831
|
+
onLog(log3(`Edge setup failed: ${msg}`, "error"));
|
|
1437
1832
|
return { component: "edge", success: false, error: msg };
|
|
1438
1833
|
}
|
|
1439
1834
|
}
|
|
1440
1835
|
var COMPONENT_INSTALLERS = {
|
|
1441
|
-
docker: (exec,
|
|
1442
|
-
// The edge is ONE component and it is a container image
|
|
1443
|
-
//
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
rsync: (exec, log5) => installRsync(exec, log5)
|
|
1836
|
+
docker: (exec, log6) => installDocker(exec, log6),
|
|
1837
|
+
// The edge is ONE component and it is a container image — there is no
|
|
1838
|
+
// host-OpenResty or host-certbot installer to reach for any more.
|
|
1839
|
+
edge: (exec, log6, config) => installContainerEdge(exec, log6, config),
|
|
1840
|
+
git: (exec, log6) => installGit(exec, log6),
|
|
1841
|
+
rsync: (exec, log6) => installRsync(exec, log6)
|
|
1448
1842
|
};
|
|
1449
1843
|
|
|
1450
1844
|
// ../../packages/adapters/src/system/available-version.ts
|
|
@@ -1572,7 +1966,7 @@ async function tryExec4(executor, command) {
|
|
|
1572
1966
|
"checks",
|
|
1573
1967
|
`exec:ok ${command} (${formatDuration(startedAt)})`
|
|
1574
1968
|
);
|
|
1575
|
-
return result;
|
|
1969
|
+
return { output: result.trim(), error: null };
|
|
1576
1970
|
} catch (err) {
|
|
1577
1971
|
if (isRemoteConnectionError(err)) {
|
|
1578
1972
|
systemDebug(
|
|
@@ -1586,9 +1980,15 @@ async function tryExec4(executor, command) {
|
|
|
1586
1980
|
"checks",
|
|
1587
1981
|
`exec:fail ${command} (${formatDuration(startedAt)}) ${msg}`
|
|
1588
1982
|
);
|
|
1589
|
-
return
|
|
1983
|
+
return { output: "", error: msg || `\`${command}\` failed` };
|
|
1590
1984
|
}
|
|
1591
1985
|
}
|
|
1986
|
+
function withReason(message, error) {
|
|
1987
|
+
if (!error) return message;
|
|
1988
|
+
const first = error.split("\n").map((line) => line.trim()).find(Boolean);
|
|
1989
|
+
if (!first) return message;
|
|
1990
|
+
return `${message} \u2014 ${first.length > 200 ? `${first.slice(0, 199)}\u2026` : first}`;
|
|
1991
|
+
}
|
|
1592
1992
|
function healthy(name, version, running) {
|
|
1593
1993
|
const component = getSystemComponentDefinition(name);
|
|
1594
1994
|
return {
|
|
@@ -1621,15 +2021,18 @@ async function checkDocker(executor) {
|
|
|
1621
2021
|
const startedAt = Date.now();
|
|
1622
2022
|
const recipe = systemCatalog.checks.docker;
|
|
1623
2023
|
const version = await tryExec4(executor, recipe.versionCommand);
|
|
1624
|
-
if (!version) {
|
|
2024
|
+
if (!version.output) {
|
|
1625
2025
|
systemDebug("checks", `docker:missing (${formatDuration(startedAt)})`);
|
|
1626
|
-
return unhealthy("docker", recipe.missingMessage);
|
|
2026
|
+
return unhealthy("docker", withReason(recipe.missingMessage, version.error));
|
|
1627
2027
|
}
|
|
1628
|
-
const parsed = recipe.parseVersion(version);
|
|
2028
|
+
const parsed = recipe.parseVersion(version.output);
|
|
1629
2029
|
const info = await tryExec4(executor, recipe.daemonCommand);
|
|
1630
|
-
if (!info) {
|
|
1631
|
-
systemDebug(
|
|
1632
|
-
|
|
2030
|
+
if (!info.output) {
|
|
2031
|
+
systemDebug(
|
|
2032
|
+
"checks",
|
|
2033
|
+
`docker:not-running (${formatDuration(startedAt)}) ${info.error ?? "probe exited 0 with no server version"}`
|
|
2034
|
+
);
|
|
2035
|
+
return unhealthy("docker", withReason(recipe.notRunningMessage, info.error), {
|
|
1633
2036
|
version: parsed,
|
|
1634
2037
|
running: false
|
|
1635
2038
|
});
|
|
@@ -1641,11 +2044,11 @@ async function checkGit(executor) {
|
|
|
1641
2044
|
const startedAt = Date.now();
|
|
1642
2045
|
const recipe = systemCatalog.checks.git;
|
|
1643
2046
|
const version = await tryExec4(executor, recipe.versionCommand);
|
|
1644
|
-
if (!version) {
|
|
2047
|
+
if (!version.output) {
|
|
1645
2048
|
systemDebug("checks", `git:missing (${formatDuration(startedAt)})`);
|
|
1646
|
-
return unhealthy("git", recipe.missingMessage);
|
|
2049
|
+
return unhealthy("git", withReason(recipe.missingMessage, version.error));
|
|
1647
2050
|
}
|
|
1648
|
-
const parsed = recipe.parseVersion(version);
|
|
2051
|
+
const parsed = recipe.parseVersion(version.output);
|
|
1649
2052
|
systemDebug("checks", `git:healthy (${formatDuration(startedAt)})`);
|
|
1650
2053
|
return healthy("git", parsed);
|
|
1651
2054
|
}
|
|
@@ -1653,11 +2056,11 @@ async function checkRsync(executor) {
|
|
|
1653
2056
|
const startedAt = Date.now();
|
|
1654
2057
|
const recipe = systemCatalog.checks.rsync;
|
|
1655
2058
|
const version = await tryExec4(executor, recipe.versionCommand);
|
|
1656
|
-
if (!version) {
|
|
2059
|
+
if (!version.output) {
|
|
1657
2060
|
systemDebug("checks", `rsync:missing (${formatDuration(startedAt)})`);
|
|
1658
|
-
return unhealthy("rsync", recipe.missingMessage);
|
|
2061
|
+
return unhealthy("rsync", withReason(recipe.missingMessage, version.error));
|
|
1659
2062
|
}
|
|
1660
|
-
const parsed = recipe.parseVersion(version);
|
|
2063
|
+
const parsed = recipe.parseVersion(version.output);
|
|
1661
2064
|
systemDebug("checks", `rsync:healthy (${formatDuration(startedAt)})`);
|
|
1662
2065
|
return healthy("rsync", parsed);
|
|
1663
2066
|
}
|
|
@@ -1671,17 +2074,29 @@ async function checkEdge(executor) {
|
|
|
1671
2074
|
"No edge on this server. The edge is the openship-edge container \u2014 install it (requires Docker)."
|
|
1672
2075
|
);
|
|
1673
2076
|
}
|
|
2077
|
+
const verdict = await verifyEdgeServing(executor, container);
|
|
2078
|
+
if (!verdict.serving) {
|
|
2079
|
+
systemDebug("checks", `edge:not-serving (${formatDuration(startedAt)})`);
|
|
2080
|
+
return unhealthy(
|
|
2081
|
+
"edge",
|
|
2082
|
+
`The edge container ${container} is not serving \u2014 ${verdict.reason ?? "it is not listening on :80"}`,
|
|
2083
|
+
{ running: false }
|
|
2084
|
+
);
|
|
2085
|
+
}
|
|
1674
2086
|
const version = await tryExec4(executor, containerCommand(container, "openresty -v 2>&1"));
|
|
1675
|
-
if (!version) {
|
|
2087
|
+
if (!version.output) {
|
|
1676
2088
|
systemDebug("checks", `edge:container-unresponsive (${formatDuration(startedAt)})`);
|
|
1677
2089
|
return unhealthy(
|
|
1678
2090
|
"edge",
|
|
1679
|
-
|
|
2091
|
+
withReason(
|
|
2092
|
+
`The edge container ${container} is serving on :80 but not answering \u2014 check \`docker logs ${container}\``,
|
|
2093
|
+
version.error
|
|
2094
|
+
),
|
|
1680
2095
|
{ running: false }
|
|
1681
2096
|
);
|
|
1682
2097
|
}
|
|
1683
2098
|
systemDebug("checks", `edge:healthy (${formatDuration(startedAt)})`);
|
|
1684
|
-
return healthy("edge", systemCatalog.checks.openresty.parseVersion(version), true);
|
|
2099
|
+
return healthy("edge", systemCatalog.checks.openresty.parseVersion(version.output), true);
|
|
1685
2100
|
}
|
|
1686
2101
|
var COMPONENT_CHECKS = {
|
|
1687
2102
|
docker: checkDocker,
|
|
@@ -1771,24 +2186,36 @@ function describeEdgeOwner(occupants) {
|
|
|
1771
2186
|
}
|
|
1772
2187
|
|
|
1773
2188
|
// ../../packages/adapters/src/system/proxy/consent.ts
|
|
1774
|
-
function
|
|
2189
|
+
function log4(message, level = "info") {
|
|
1775
2190
|
return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
|
|
1776
2191
|
}
|
|
1777
2192
|
async function ensureEdgeClear(executor, config, onLog) {
|
|
1778
2193
|
const status = await probeEdge(executor);
|
|
1779
|
-
if (status.canProceedClean) return { tookOver: false };
|
|
2194
|
+
if (status.canProceedClean) return { tookOver: false, status };
|
|
1780
2195
|
const takeover = async () => {
|
|
1781
|
-
onLog(
|
|
2196
|
+
onLog(log4(
|
|
1782
2197
|
`Taking over ports from ${status.occupants.map((o) => o.command ?? o.port).join(", ")}...`,
|
|
1783
2198
|
"warn"
|
|
1784
2199
|
));
|
|
1785
|
-
const
|
|
1786
|
-
|
|
1787
|
-
|
|
2200
|
+
const freed = await beginEdgeTakeover(
|
|
2201
|
+
executor,
|
|
2202
|
+
status,
|
|
2203
|
+
onLog,
|
|
2204
|
+
config?.edgePolicy?.stopTargets ?? []
|
|
2205
|
+
);
|
|
2206
|
+
if (!freed.freed) {
|
|
2207
|
+
await rollbackEdgeTakeover(executor, onLog);
|
|
2208
|
+
const plural = freed.stillBound.length > 1;
|
|
2209
|
+
throw new AppError(
|
|
2210
|
+
`Stopped the existing proxy, but port${plural ? "s" : ""} ${freed.stillBound.join(" and ")} ${plural ? "are" : "is"} still in use, so the edge can't bind. Nothing was installed and the previous proxy has been restored \u2014 find what else is holding the port and retry.`,
|
|
2211
|
+
409,
|
|
2212
|
+
"EDGE_PORTS_STILL_BOUND"
|
|
2213
|
+
);
|
|
2214
|
+
}
|
|
1788
2215
|
};
|
|
1789
2216
|
if (config?.edgePolicy?.mode === "takeover") {
|
|
1790
2217
|
await takeover();
|
|
1791
|
-
return { tookOver: true };
|
|
2218
|
+
return { tookOver: true, status };
|
|
1792
2219
|
}
|
|
1793
2220
|
if (config?.promptUser) {
|
|
1794
2221
|
const known = status.classification === "known";
|
|
@@ -1817,7 +2244,7 @@ async function ensureEdgeClear(executor, config, onLog) {
|
|
|
1817
2244
|
}
|
|
1818
2245
|
if (action === "override") {
|
|
1819
2246
|
await takeover();
|
|
1820
|
-
return { tookOver: true };
|
|
2247
|
+
return { tookOver: true, status };
|
|
1821
2248
|
}
|
|
1822
2249
|
throw new EdgeConflictError(status);
|
|
1823
2250
|
}
|
|
@@ -1825,7 +2252,7 @@ async function ensureEdgeClear(executor, config, onLog) {
|
|
|
1825
2252
|
}
|
|
1826
2253
|
|
|
1827
2254
|
// ../../packages/adapters/src/system/proxy/ensure-container-edge.ts
|
|
1828
|
-
function
|
|
2255
|
+
function log5(message, level = "info") {
|
|
1829
2256
|
return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
|
|
1830
2257
|
}
|
|
1831
2258
|
var injectedDefaultImage;
|
|
@@ -1835,15 +2262,8 @@ function setDefaultEdgeImage(image) {
|
|
|
1835
2262
|
function resolveEdgeImage(explicit) {
|
|
1836
2263
|
return buildEdgeImageRef({ explicit, injectedDefault: injectedDefaultImage });
|
|
1837
2264
|
}
|
|
1838
|
-
async function dockerAvailable(executor) {
|
|
1839
|
-
return executor.exec("docker version --format '{{.Server.Version}}' 2>/dev/null").then((v) => Boolean(v.trim())).catch(() => false);
|
|
1840
|
-
}
|
|
1841
|
-
async function runningImage(executor, container) {
|
|
1842
|
-
const out = await executor.exec(`docker inspect -f '{{.Config.Image}}' ${sq(container)} 2>/dev/null`).catch(() => "");
|
|
1843
|
-
return out.trim() || null;
|
|
1844
|
-
}
|
|
1845
2265
|
async function containerEdgeProvider(executor, container, opts) {
|
|
1846
|
-
const { NginxProvider: Provider } = await import("./nginx-
|
|
2266
|
+
const { NginxProvider: Provider } = await import("./nginx-AXGNWRKZ.js");
|
|
1847
2267
|
return new Provider({
|
|
1848
2268
|
...opts,
|
|
1849
2269
|
// Vhosts go to the BIND-MOUNTED HOST dir (so every host-side reader — the
|
|
@@ -1851,21 +2271,33 @@ async function containerEdgeProvider(executor, container, opts) {
|
|
|
1851
2271
|
// while reload/certbot run inside the container.
|
|
1852
2272
|
paths: EDGE_HOST_PATHS,
|
|
1853
2273
|
executor: edgeContainerExecutor(executor, container),
|
|
2274
|
+
// Challenge tokens go to the HOST side of the ACME mount, for the same reason
|
|
2275
|
+
// vhosts do. This is the ONE mount whose two sides differ
|
|
2276
|
+
// (/var/lib/openship/edge/acme → /var/www/acme), so the write path and the
|
|
2277
|
+
// vhost's `root` are genuinely different strings — `root` stays the container
|
|
2278
|
+
// path. Overridden here and nowhere else, next to the `paths` override it
|
|
2279
|
+
// mirrors, so the two cannot drift.
|
|
2280
|
+
challengeDir: EDGE_CHALLENGE_HOST_DIR,
|
|
1854
2281
|
// MUST be pinned: re-detection answers from inside the container and would
|
|
1855
2282
|
// repoint sitesDir at a host dir the edge never reads.
|
|
1856
|
-
pinPaths: true
|
|
2283
|
+
pinPaths: true,
|
|
2284
|
+
// Reload runs INSIDE the container, where the master is pid 1 — so a failed
|
|
2285
|
+
// reload must never fall back to killing it (#292).
|
|
2286
|
+
containerEdge: true
|
|
1857
2287
|
});
|
|
1858
2288
|
}
|
|
1859
2289
|
async function localContainerEdgeProvider(container, opts) {
|
|
1860
|
-
const { DockerEdgeExecutor } = await import("./docker-edge-executor-
|
|
1861
|
-
const { NginxProvider: Provider } = await import("./nginx-
|
|
2290
|
+
const { DockerEdgeExecutor } = await import("./docker-edge-executor-62N64MGS.js");
|
|
2291
|
+
const { NginxProvider: Provider } = await import("./nginx-AXGNWRKZ.js");
|
|
1862
2292
|
return new Provider({
|
|
1863
2293
|
...opts,
|
|
1864
2294
|
paths: OPENRESTY_DEFAULT_PATHS,
|
|
1865
2295
|
executor: new DockerEdgeExecutor({ containerName: container }),
|
|
1866
2296
|
// nginx.conf + the Lua are BAKED into the image — nothing to detect, install
|
|
1867
2297
|
// or patch, and detection would answer from inside the container anyway.
|
|
1868
|
-
pinPaths: true
|
|
2298
|
+
pinPaths: true,
|
|
2299
|
+
// `docker exec`s into the edge, so the master is pid 1 there too (#292).
|
|
2300
|
+
containerEdge: true
|
|
1869
2301
|
});
|
|
1870
2302
|
}
|
|
1871
2303
|
async function startEdgeContainer(executor, container, image, onLog) {
|
|
@@ -1894,37 +2326,39 @@ function buildEdgeRunCommand(container, image) {
|
|
|
1894
2326
|
sq(image)
|
|
1895
2327
|
].join(" ");
|
|
1896
2328
|
}
|
|
1897
|
-
|
|
2329
|
+
function makeEdgeStart(executor, container, opts) {
|
|
1898
2330
|
const { onLog } = opts;
|
|
1899
|
-
|
|
1900
|
-
const pull = await executor.streamExec(`docker pull ${sq(to)}`, onLog);
|
|
1901
|
-
if (pull.code !== 0) {
|
|
1902
|
-
onLog(log4(`Could not pull ${to} \u2014 the edge stays on ${from}.`, "warn"));
|
|
1903
|
-
return { swapped: false, edgeDown: false };
|
|
1904
|
-
}
|
|
1905
|
-
const start = async (image) => {
|
|
2331
|
+
return async (image) => {
|
|
1906
2332
|
if (!await startEdgeContainer(executor, container, image, onLog)) return false;
|
|
1907
|
-
const
|
|
2333
|
+
const verdict = await verifyEdgeServing(executor, container, {
|
|
1908
2334
|
timeoutMs: opts.verifyTimeoutMs ?? 3e4
|
|
1909
2335
|
});
|
|
1910
|
-
|
|
2336
|
+
if (!verdict.serving && verdict.reason) onLog(log5(`Edge ${image}: ${verdict.reason}`, "warn"));
|
|
2337
|
+
return verdict.serving;
|
|
1911
2338
|
};
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
2339
|
+
}
|
|
2340
|
+
async function verifyEdgeServing(executor, container, opts = {}) {
|
|
2341
|
+
if (await edgeIsBroken(executor, container)) {
|
|
2342
|
+
const reason = await edgeCrashReason(executor, container);
|
|
2343
|
+
return { serving: false, reason: reason ?? "the edge container is not running" };
|
|
1915
2344
|
}
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
2345
|
+
if (opts.configTest) {
|
|
2346
|
+
try {
|
|
2347
|
+
await executor.exec(containerCommand(container, "openresty -t 2>&1"));
|
|
2348
|
+
} catch (err) {
|
|
2349
|
+
const out = safeErrorMessage(err);
|
|
2350
|
+
return { serving: false, reason: edgeFailureReason(out) ?? out };
|
|
2351
|
+
}
|
|
1919
2352
|
}
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
2353
|
+
const listening = await waitForPortListening(executor, 80, { timeoutMs: opts.timeoutMs ?? 0 });
|
|
2354
|
+
if (listening.checked && !listening.listening) {
|
|
2355
|
+
const reason = await edgeCrashReason(executor, container);
|
|
2356
|
+
return {
|
|
2357
|
+
serving: false,
|
|
2358
|
+
reason: reason ?? "the edge container is up but nothing is listening on :80"
|
|
2359
|
+
};
|
|
2360
|
+
}
|
|
2361
|
+
return { serving: true };
|
|
1928
2362
|
}
|
|
1929
2363
|
async function ensureContainerEdge(executor, opts) {
|
|
1930
2364
|
const { onLog } = opts;
|
|
@@ -1932,123 +2366,112 @@ async function ensureContainerEdge(executor, opts) {
|
|
|
1932
2366
|
const image = resolveEdgeImage(opts.image);
|
|
1933
2367
|
const existing = await resolveOurEdgeContainer(executor, { fresh: true });
|
|
1934
2368
|
if (existing) {
|
|
1935
|
-
const
|
|
1936
|
-
if (
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
2369
|
+
const verdict = await verifyEdgeServing(executor, existing);
|
|
2370
|
+
if (!verdict.serving) {
|
|
2371
|
+
onLog(log5(`The edge container exists but isn't serving: ${verdict.reason}`, "warn"));
|
|
2372
|
+
onLog(log5("Recreating it...", "warn"));
|
|
2373
|
+
} else {
|
|
2374
|
+
const current = await containerImageRef(executor, existing);
|
|
2375
|
+
const stale = current != null && current !== image;
|
|
2376
|
+
if (stale) {
|
|
2377
|
+
const start = makeEdgeStart(executor, existing, opts);
|
|
2378
|
+
const swap = await swapManagedImage(executor, {
|
|
2379
|
+
kind: "edge",
|
|
2380
|
+
from: current ?? image,
|
|
2381
|
+
to: image,
|
|
2382
|
+
label: "edge",
|
|
2383
|
+
onLog,
|
|
2384
|
+
start
|
|
2385
|
+
});
|
|
2386
|
+
return {
|
|
2387
|
+
container: existing,
|
|
2388
|
+
image,
|
|
2389
|
+
converted: false,
|
|
2390
|
+
updated: swap.swapped,
|
|
2391
|
+
edgeDown: swap.down
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2394
|
+
return { container: existing, image, converted: false };
|
|
1945
2395
|
}
|
|
1946
|
-
return { container: existing, image, converted: false };
|
|
1947
2396
|
}
|
|
1948
|
-
await ensureEdgeClear(executor, opts.config, onLog);
|
|
1949
2397
|
if (!await dockerAvailable(executor)) {
|
|
1950
2398
|
throw new Error(
|
|
1951
2399
|
"Docker isn't available on this server, and the edge now runs as a container. Install the Docker component first, then set up the edge."
|
|
1952
2400
|
);
|
|
1953
2401
|
}
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
)
|
|
2402
|
+
if (!await imageExistsLocally(executor, image)) {
|
|
2403
|
+
if (managedImagesAreFromSource("edge")) {
|
|
2404
|
+
throw new Error(fromSourceImageMissingMessage("edge", image));
|
|
2405
|
+
}
|
|
2406
|
+
onLog(log5(`Pulling edge image ${image}...`));
|
|
2407
|
+
const pull = await executor.streamExec(`docker pull ${sq(image)}`, onLog);
|
|
2408
|
+
if (pull.code !== 0) {
|
|
2409
|
+
throw new Error(
|
|
2410
|
+
`Could not pull the edge image ${image} \u2014 the box is unchanged and still serving. Check the server's network access to the registry, then retry.`
|
|
2411
|
+
);
|
|
2412
|
+
}
|
|
1961
2413
|
}
|
|
1962
2414
|
for (const mount of EDGE_CONTAINER_MOUNTS) {
|
|
1963
2415
|
await executor.exec(`mkdir -p ${sq(mount.host)}`).catch(() => {
|
|
1964
2416
|
});
|
|
1965
2417
|
}
|
|
1966
|
-
const
|
|
1967
|
-
let beforeCarry = null;
|
|
1968
|
-
if (bareWasOurs) {
|
|
1969
|
-
const barePaths = await detectOpenRestyPaths(executor).catch(() => OPENRESTY_DEFAULT_PATHS);
|
|
1970
|
-
if (barePaths.sitesDir !== sitesTarget && await executor.exists(barePaths.sitesDir)) {
|
|
1971
|
-
onLog(log4(`Carrying vhosts from ${barePaths.sitesDir}...`));
|
|
1972
|
-
beforeCarry = await listDir(executor, sitesTarget);
|
|
1973
|
-
await executor.exec(`cp -a ${sq(`${barePaths.sitesDir}/.`)} ${sq(`${sitesTarget}/`)} 2>/dev/null || true`).catch(() => {
|
|
1974
|
-
});
|
|
1975
|
-
}
|
|
1976
|
-
}
|
|
1977
|
-
const restoreBare = async () => {
|
|
1978
|
-
if (!bareWasOurs) return;
|
|
1979
|
-
if (beforeCarry) {
|
|
1980
|
-
const now = await listDir(executor, sitesTarget);
|
|
1981
|
-
const added = [...now].filter((name) => !beforeCarry.has(name));
|
|
1982
|
-
if (added.length > 0) {
|
|
1983
|
-
await executor.exec(`rm -f ${added.map((n) => sq(`${sitesTarget}/${n}`)).join(" ")}`).catch(() => {
|
|
1984
|
-
});
|
|
1985
|
-
onLog(log4(`Removed ${added.length} carried vhost(s) so the next edge start is clean.`, "warn"));
|
|
1986
|
-
}
|
|
1987
|
-
}
|
|
1988
|
-
onLog(log4("Restoring the host OpenResty edge...", "warn"));
|
|
1989
|
-
await executor.exec("systemctl enable --now openresty 2>/dev/null || true").catch(() => {
|
|
1990
|
-
});
|
|
1991
|
-
};
|
|
2418
|
+
const clear = await ensureEdgeClear(executor, opts.config, onLog);
|
|
1992
2419
|
try {
|
|
1993
|
-
|
|
1994
|
-
onLog(log4("Stopping the host OpenResty edge..."));
|
|
1995
|
-
await executor.exec("systemctl disable --now openresty 2>/dev/null || true").catch(() => {
|
|
1996
|
-
});
|
|
1997
|
-
await executor.exec("systemctl reset-failed openresty 2>/dev/null || true").catch(() => {
|
|
1998
|
-
});
|
|
1999
|
-
}
|
|
2000
|
-
onLog(log4("Starting the edge container..."));
|
|
2420
|
+
onLog(log5("Starting the edge container..."));
|
|
2001
2421
|
if (!await startEdgeContainer(executor, container, image, onLog)) {
|
|
2002
2422
|
throw new Error("the edge container failed to start");
|
|
2003
2423
|
}
|
|
2004
|
-
await executor
|
|
2005
|
-
|
|
2424
|
+
const verdict = await verifyEdgeServing(executor, container, {
|
|
2425
|
+
configTest: true,
|
|
2006
2426
|
timeoutMs: opts.verifyTimeoutMs ?? 3e4
|
|
2007
2427
|
});
|
|
2008
|
-
if (
|
|
2009
|
-
throw new Error("the edge container started but
|
|
2428
|
+
if (!verdict.serving) {
|
|
2429
|
+
throw new Error(verdict.reason ?? "the edge container started but isn't serving");
|
|
2010
2430
|
}
|
|
2431
|
+
if (clear.tookOver) await completeEdgeTakeover(executor);
|
|
2011
2432
|
const version = await executor.exec(containerCommand(container, "openresty -v 2>&1")).catch(() => "");
|
|
2012
|
-
onLog(
|
|
2013
|
-
return { container, image, converted:
|
|
2433
|
+
onLog(log5(`Edge container running${version.trim() ? ` (${version.trim()})` : ""}`));
|
|
2434
|
+
return { container, image, converted: clear.tookOver };
|
|
2014
2435
|
} catch (err) {
|
|
2015
2436
|
const msg = safeErrorMessage(err);
|
|
2016
|
-
onLog(
|
|
2437
|
+
onLog(log5(`Edge container setup failed: ${msg}`, "error"));
|
|
2017
2438
|
const logs = await executor.exec(`docker logs --tail 40 ${sq(container)} 2>&1 || true`).catch(() => "");
|
|
2018
|
-
if (logs.trim()) onLog(
|
|
2439
|
+
if (logs.trim()) onLog(log5(`Edge container logs:
|
|
2019
2440
|
${logs}`, "error"));
|
|
2020
2441
|
await executor.exec(`docker rm -f ${sq(container)} 2>/dev/null || true`).catch(() => {
|
|
2021
2442
|
});
|
|
2022
2443
|
invalidateEdgeContainer(executor);
|
|
2023
|
-
await
|
|
2444
|
+
if (clear.tookOver) await rollbackEdgeTakeover(executor, onLog);
|
|
2024
2445
|
const reason = edgeFailureReason(logs);
|
|
2025
2446
|
throw new Error(`Edge container setup failed: ${reason ? `${reason} (${msg})` : msg}`);
|
|
2026
2447
|
}
|
|
2027
2448
|
}
|
|
2028
2449
|
|
|
2029
2450
|
export {
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2451
|
+
beginEdgeTakeover,
|
|
2452
|
+
rollbackEdgeTakeover,
|
|
2453
|
+
completeEdgeTakeover,
|
|
2454
|
+
recoverInterruptedTakeover,
|
|
2034
2455
|
detectInstalledProxy,
|
|
2035
2456
|
scanImportableSites,
|
|
2457
|
+
unreachableStaticRoots,
|
|
2458
|
+
copyStaticRootIntoEdge,
|
|
2036
2459
|
edgeProxy,
|
|
2037
2460
|
edgeProxyFor,
|
|
2038
2461
|
collectProxyCerts,
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2462
|
+
systemCatalog,
|
|
2463
|
+
installGit,
|
|
2464
|
+
COMPONENT_INSTALLERS,
|
|
2465
|
+
importSites,
|
|
2466
|
+
foreignProxyOnEdge,
|
|
2043
2467
|
setDefaultEdgeImage,
|
|
2044
2468
|
resolveEdgeImage,
|
|
2045
|
-
dockerAvailable,
|
|
2046
2469
|
containerEdgeProvider,
|
|
2047
2470
|
localContainerEdgeProvider,
|
|
2048
2471
|
buildEdgeRunCommand,
|
|
2472
|
+
verifyEdgeServing,
|
|
2049
2473
|
ensureContainerEdge,
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
COMPONENT_INSTALLERS
|
|
2474
|
+
checkGit,
|
|
2475
|
+
checkAll,
|
|
2476
|
+
checkComponents
|
|
2054
2477
|
};
|