openship 0.5.0 → 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 +26 -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-ZCOQJCZO.js → chunk-6ODUBKUB.js} +3 -3
- package/dist/{chunk-DYPH4HCI.js → chunk-AO252K7Z.js} +1 -1
- package/dist/{chunk-K2RPVGPN.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-V7BZMGOT.js → chunk-EMXI4UCU.js} +5 -5
- package/dist/{chunk-NNPEG6HM.js → chunk-EP2TNALB.js} +2 -2
- package/dist/{chunk-NXFIRK2Z.js → chunk-GUQ3QCJ7.js} +573 -459
- package/dist/chunk-I3DPMTSF.js +29 -0
- package/dist/{chunk-KZOBYJUW.js → chunk-IW2UKBVF.js} +3 -3
- package/dist/{chunk-YWMODKWL.js → chunk-IXM5245F.js} +165 -13
- package/dist/{chunk-KNTJ7PSF.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-UMQP7SYT.js → chunk-OTLSJYAC.js} +1316 -53
- package/dist/{chunk-EI575GHN.js → chunk-PDW4L3NV.js} +3 -3
- package/dist/{chunk-JLKKYUZA.js → chunk-PSZSKEPA.js} +7 -7
- package/dist/{chunk-KI2EY3WB.js → chunk-PXKCZB2I.js} +1 -1
- package/dist/{chunk-RB35YN3J.js → chunk-QEH4QUBX.js} +391 -63
- 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-6LKHXB6T.js → chunk-TWJCLD4W.js} +48 -13
- package/dist/{chunk-I6PSTQIR.js → chunk-VDDMADOF.js} +3 -3
- package/dist/{chunk-BS2AXYZX.js → chunk-VE2YOK4I.js} +626 -214
- package/dist/{chunk-6PIYRG5I.js → chunk-VZ2ZHPS2.js} +52 -42
- 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-BAJMY6KM.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-LOYMPU67.js → executor-JELARVKC.js} +8 -8
- package/dist/index.js +1170 -459
- 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 +23172 -7994
- 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/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 +112 -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-VRGE5MXH.js +0 -23
- package/dist/chunk-CUJXGK7R.js +0 -3167
- package/dist/chunk-EAAGSQXN.js +0 -377
- package/dist/cloud-3M2WZ624.js +0 -30
- package/dist/docker-2NGBRJFJ.js +0 -18
- package/dist/docker-edge-executor-5I4M5QT7.js +0 -13
- package/dist/edge-import-RIWNEIXQ.js +0 -47
- package/dist/ensure-container-edge-3NSYKHRX.js +0 -31
- package/dist/nginx-QE4DOM5J.js +0 -24
- package/dist/openresty-lua-BTAKECCF.js +0 -42
- package/dist/setup-KELALK7H.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
|
-
|
|
43
|
+
OPENRESTY_DEFAULT_PATHS
|
|
44
|
+
} from "./chunk-OGY63DGD.js";
|
|
45
|
+
import {
|
|
46
|
+
sq
|
|
47
|
+
} from "./chunk-ZFJXPCQP.js";
|
|
47
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 = [];
|
|
@@ -105,10 +328,17 @@ function sq3(value) {
|
|
|
105
328
|
}
|
|
106
329
|
|
|
107
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
|
+
}
|
|
108
338
|
async function loadNginxConfig(executor) {
|
|
109
|
-
const dumped = await
|
|
110
|
-
if (dumped
|
|
111
|
-
const cat = await
|
|
339
|
+
const dumped = await dumpResolvedConfig(executor, ["nginx", "openresty"]);
|
|
340
|
+
if (dumped) return dumped;
|
|
341
|
+
const cat = await tryExec2(
|
|
112
342
|
executor,
|
|
113
343
|
"cat /etc/nginx/nginx.conf /etc/nginx/sites-enabled/* /etc/nginx/conf.d/*.conf 2>/dev/null"
|
|
114
344
|
);
|
|
@@ -146,7 +376,7 @@ function resolveProxyTarget(proxyPass, upstreams) {
|
|
|
146
376
|
if (isIp || host.includes(".")) return { url: raw };
|
|
147
377
|
return { reason: `proxy_pass host "${host}" is an undeclared upstream \u2014 not migratable` };
|
|
148
378
|
}
|
|
149
|
-
function
|
|
379
|
+
function locationBlocks(serverBody) {
|
|
150
380
|
const out = [];
|
|
151
381
|
const re = /(?:^|[\s;}])location\s+([^{]+?)\s*\{/g;
|
|
152
382
|
let m;
|
|
@@ -160,13 +390,26 @@ function extractLocationProxies(serverBody) {
|
|
|
160
390
|
else if (serverBody[i] === "}") depth--;
|
|
161
391
|
}
|
|
162
392
|
if (depth !== 0) break;
|
|
163
|
-
|
|
393
|
+
out.push({ path, body: serverBody.slice(openIdx + 1, i - 1) });
|
|
164
394
|
re.lastIndex = i;
|
|
165
|
-
const pp = firstDirective(locBody, "proxy_pass");
|
|
166
|
-
if (pp) out.push({ path, proxyPass: pp });
|
|
167
395
|
}
|
|
168
396
|
return out;
|
|
169
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
|
+
}
|
|
170
413
|
function isHttpsUpgradeForSelf(body, names) {
|
|
171
414
|
const targets = [
|
|
172
415
|
...body.matchAll(/(?:^|[\s;{])return\s+30[1-8]\s+([^;]+);/g),
|
|
@@ -181,15 +424,31 @@ function isHttpsUpgradeForSelf(body, names) {
|
|
|
181
424
|
return names.includes(host);
|
|
182
425
|
});
|
|
183
426
|
}
|
|
427
|
+
var CHALLENGE_LOCATION_RE = /\.well-known\/(acme-challenge|oblien-proxy-challenge)/i;
|
|
184
428
|
function isAcmeWebrootOnly(body) {
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
);
|
|
188
|
-
if (paths.length === 0) {
|
|
429
|
+
const locations = locationBlocks(body);
|
|
430
|
+
if (locations.length === 0) {
|
|
189
431
|
const root = firstDirective(body, "root") ?? "";
|
|
190
432
|
return /letsencrypt|acme|certbot/i.test(root);
|
|
191
433
|
}
|
|
192
|
-
|
|
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
|
+
};
|
|
193
452
|
}
|
|
194
453
|
function parseServer(body, source, upstreams) {
|
|
195
454
|
const warnings = [];
|
|
@@ -230,6 +489,9 @@ function parseServer(body, source, upstreams) {
|
|
|
230
489
|
const site = { serverNames: names, ssl, target, source };
|
|
231
490
|
if (routes) site.routes = routes;
|
|
232
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;
|
|
233
495
|
return { site, warnings };
|
|
234
496
|
}
|
|
235
497
|
function parseNginxConfig(raw) {
|
|
@@ -264,21 +526,31 @@ var OUR_EDGE_SITE_GLOBS = [
|
|
|
264
526
|
`${OPENRESTY_DEFAULT_PATHS.sitesDir}/*.conf`,
|
|
265
527
|
"/etc/openresty/sites-enabled/*.conf"
|
|
266
528
|
];
|
|
529
|
+
async function scanForeignOpenResty(executor) {
|
|
530
|
+
const dump = await dumpResolvedConfig(executor, ["openresty"]);
|
|
531
|
+
if (dump) return parseNginxConfig(dump);
|
|
532
|
+
return scanOpenshipEdge(executor);
|
|
533
|
+
}
|
|
267
534
|
async function scanOpenshipEdge(executor) {
|
|
268
535
|
const cat = `cat ${OUR_EDGE_SITE_GLOBS.join(" ")} 2>/dev/null`;
|
|
269
|
-
const raw = await
|
|
536
|
+
const raw = await tryExec2(executor, cat);
|
|
270
537
|
if (raw?.trim()) return parseNginxConfig(raw);
|
|
271
538
|
const container = await resolveOurEdgeContainer(executor);
|
|
272
539
|
if (!container) return parseNginxConfig("");
|
|
273
|
-
const fromContainer = await
|
|
540
|
+
const fromContainer = await tryExec2(executor, containerCommand(container, cat));
|
|
274
541
|
return parseNginxConfig(fromContainer ?? "");
|
|
275
542
|
}
|
|
276
543
|
|
|
277
544
|
// ../../packages/adapters/src/system/proxy/import/caddy.ts
|
|
278
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
|
+
}
|
|
279
551
|
async function loadCaddyJson(executor) {
|
|
280
552
|
for (const p of CADDYFILE_PATHS) {
|
|
281
|
-
const out = await
|
|
553
|
+
const out = await tryExec2(executor, `caddy adapt --adapter caddyfile --config ${p} 2>/dev/null`);
|
|
282
554
|
if (out && out.trim().startsWith("{")) return out;
|
|
283
555
|
}
|
|
284
556
|
return null;
|
|
@@ -393,7 +665,7 @@ function parseCaddyJson(raw) {
|
|
|
393
665
|
}
|
|
394
666
|
async function loadCaddyfile(executor) {
|
|
395
667
|
for (const p of CADDYFILE_PATHS) {
|
|
396
|
-
const out = await
|
|
668
|
+
const out = await tryExec2(executor, `cat ${p} 2>/dev/null`);
|
|
397
669
|
if (out && out.trim()) return out;
|
|
398
670
|
}
|
|
399
671
|
return "";
|
|
@@ -430,8 +702,9 @@ function parseAddresses(header) {
|
|
|
430
702
|
}).filter((a) => a.host && !a.host.startsWith(":") && a.host !== "*");
|
|
431
703
|
}
|
|
432
704
|
async function scanCaddy(executor) {
|
|
433
|
-
const
|
|
434
|
-
|
|
705
|
+
for (const load of [loadCaddyAdminConfig, loadCaddyJson]) {
|
|
706
|
+
const json = await load(executor);
|
|
707
|
+
if (!json) continue;
|
|
435
708
|
const parsed = parseCaddyJson(json);
|
|
436
709
|
if (parsed && (parsed.sites.length > 0 || parsed.warnings.length > 0)) {
|
|
437
710
|
return { proxy: "caddy", sites: parsed.sites, warnings: parsed.warnings };
|
|
@@ -527,7 +800,7 @@ async function scanCaddyText(executor) {
|
|
|
527
800
|
|
|
528
801
|
// ../../packages/adapters/src/system/proxy/import/apache.ts
|
|
529
802
|
async function vhostFilesFromDump(executor) {
|
|
530
|
-
const dump = await
|
|
803
|
+
const dump = await tryExec2(
|
|
531
804
|
executor,
|
|
532
805
|
"apachectl -S 2>/dev/null || apache2ctl -S 2>/dev/null || httpd -S 2>/dev/null"
|
|
533
806
|
);
|
|
@@ -542,18 +815,22 @@ async function vhostFilesFromDump(executor) {
|
|
|
542
815
|
async function loadApacheConfig(executor) {
|
|
543
816
|
const files = await vhostFilesFromDump(executor);
|
|
544
817
|
if (files.length > 0) {
|
|
545
|
-
const cat2 = await
|
|
818
|
+
const cat2 = await tryExec2(executor, `cat ${files.map(sq3).join(" ")} 2>/dev/null`);
|
|
546
819
|
if (cat2 && cat2.trim()) return cat2;
|
|
547
820
|
}
|
|
548
|
-
const cat = await
|
|
821
|
+
const cat = await tryExec2(
|
|
549
822
|
executor,
|
|
550
823
|
"cat /etc/apache2/sites-enabled/*.conf /etc/apache2/apache2.conf /etc/httpd/conf.d/*.conf /etc/httpd/conf/httpd.conf 2>/dev/null"
|
|
551
824
|
);
|
|
552
825
|
return cat ?? "";
|
|
553
826
|
}
|
|
827
|
+
function unquote(value) {
|
|
828
|
+
return value.replace(/^["']|["']$/g, "");
|
|
829
|
+
}
|
|
554
830
|
function directive(body, name) {
|
|
555
831
|
const m = body.match(new RegExp(`(?:^|\\n)\\s*${name}\\s+([^\\n]+)`, "i"));
|
|
556
|
-
|
|
832
|
+
const value = m?.[1]?.trim();
|
|
833
|
+
return value === void 0 ? void 0 : unquote(value);
|
|
557
834
|
}
|
|
558
835
|
function directiveAll(body, name) {
|
|
559
836
|
return [...body.matchAll(new RegExp(`(?:^|\\n)\\s*${name}\\s+([^\\n]+)`, "gi"))].map(
|
|
@@ -570,7 +847,36 @@ function isHttpsRedirectOnly(body) {
|
|
|
570
847
|
return redirects.length > 0 && redirects.some((t) => /^https:\/\//i.test(t));
|
|
571
848
|
}
|
|
572
849
|
function hostOnly(name) {
|
|
573
|
-
return name.trim().replace(/^https?:\/\//i, "").replace(/:\d+$/, "");
|
|
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 };
|
|
574
880
|
}
|
|
575
881
|
function parseVhost(body, portHint) {
|
|
576
882
|
const serverName = directive(body, "ServerName")?.split(/\s+/)[0];
|
|
@@ -580,8 +886,9 @@ function parseVhost(body, portHint) {
|
|
|
580
886
|
return { warning: "apache: skipped a VirtualHost with no ServerName" };
|
|
581
887
|
}
|
|
582
888
|
if (isHttpsRedirectOnly(body)) return null;
|
|
583
|
-
const
|
|
584
|
-
const
|
|
889
|
+
const proxy = proxyRoutes(body);
|
|
890
|
+
const routes = proxy.routes.filter((p) => p.url && p.url !== "!");
|
|
891
|
+
const docRoot = directive(body, "DocumentRoot");
|
|
585
892
|
const fcgiMatch = /(?:^|\n)\s*ProxyPassMatch\s+[^\n]*(fcgi:|php-fpm|php\d*-fpm|\.sock)/i.test(body);
|
|
586
893
|
const certPath = directive(body, "SSLCertificateFile");
|
|
587
894
|
const keyPath = directive(body, "SSLCertificateKeyFile");
|
|
@@ -605,6 +912,10 @@ function parseVhost(body, portHint) {
|
|
|
605
912
|
return {
|
|
606
913
|
warning: `apache: ${names[0]} uses ProxyPassMatch (regex proxying) \u2014 re-add it manually`
|
|
607
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
|
+
};
|
|
608
919
|
} else {
|
|
609
920
|
return { warning: `apache: ${names[0]} has neither ProxyPass nor DocumentRoot \u2014 skipped` };
|
|
610
921
|
}
|
|
@@ -834,7 +1145,7 @@ function parseTraefikLabels(containers) {
|
|
|
834
1145
|
};
|
|
835
1146
|
}
|
|
836
1147
|
async function scanTraefik(executor) {
|
|
837
|
-
const out = await
|
|
1148
|
+
const out = await tryExec2(
|
|
838
1149
|
executor,
|
|
839
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`
|
|
840
1151
|
);
|
|
@@ -916,7 +1227,7 @@ async function scanImportableSites(executor, proxy) {
|
|
|
916
1227
|
case "traefik":
|
|
917
1228
|
return scanTraefik(executor);
|
|
918
1229
|
case "openresty":
|
|
919
|
-
return
|
|
1230
|
+
return scanForeignOpenResty(executor);
|
|
920
1231
|
default:
|
|
921
1232
|
return {
|
|
922
1233
|
proxy,
|
|
@@ -928,6 +1239,27 @@ async function scanImportableSites(executor, proxy) {
|
|
|
928
1239
|
function canImportProxy(proxy) {
|
|
929
1240
|
return proxy === "nginx" || proxy === "caddy" || proxy === "apache" || proxy === "traefik" || proxy === "openresty";
|
|
930
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
|
+
}
|
|
931
1263
|
|
|
932
1264
|
// ../../packages/adapters/src/system/proxy/import/caddy-certs.ts
|
|
933
1265
|
var CADDY_DATA_ROOTS = [
|
|
@@ -945,10 +1277,10 @@ function candidateNames(host) {
|
|
|
945
1277
|
async function findPairIn(exec, root, host, container) {
|
|
946
1278
|
for (const name of candidateNames(host)) {
|
|
947
1279
|
const glob = `${root}/certificates/*/${name}/${name}.crt`;
|
|
948
|
-
let certPath = firstCrt(await
|
|
1280
|
+
let certPath = firstCrt(await tryExec2(exec, `ls -1 ${glob} 2>/dev/null`));
|
|
949
1281
|
if (!certPath && container) {
|
|
950
1282
|
certPath = firstCrt(
|
|
951
|
-
await
|
|
1283
|
+
await tryExec2(exec, containerCommand(container, `ls -1 ${glob} 2>/dev/null`))
|
|
952
1284
|
);
|
|
953
1285
|
}
|
|
954
1286
|
if (certPath) return { certPath, keyPath: certPath.replace(/\.crt$/, ".key") };
|
|
@@ -994,7 +1326,7 @@ async function candidateStoragePaths(exec, container) {
|
|
|
994
1326
|
};
|
|
995
1327
|
if (container) {
|
|
996
1328
|
collect(
|
|
997
|
-
await
|
|
1329
|
+
await tryExec2(
|
|
998
1330
|
exec,
|
|
999
1331
|
`docker inspect ${sq2(container)} --format '{{json .Config.Cmd}} {{json .Args}} {{json .Config.Env}}' 2>/dev/null`
|
|
1000
1332
|
)
|
|
@@ -1098,11 +1430,13 @@ function buildProxyRouteIndex(sites) {
|
|
|
1098
1430
|
existing.ssl.keyPath = site.tls.keyPath;
|
|
1099
1431
|
}
|
|
1100
1432
|
}
|
|
1433
|
+
if (site.proxy && (site.ssl || !existing.proxy)) existing.proxy = site.proxy;
|
|
1101
1434
|
} else {
|
|
1102
1435
|
list.push({
|
|
1103
1436
|
port,
|
|
1104
1437
|
path: up.path,
|
|
1105
1438
|
domains: [...site.serverNames],
|
|
1439
|
+
...site.proxy ? { proxy: site.proxy } : {},
|
|
1106
1440
|
ssl: site.ssl ? { enabled: true, certPath: site.tls?.certPath, keyPath: site.tls?.keyPath } : { enabled: false },
|
|
1107
1441
|
source: site.source
|
|
1108
1442
|
});
|
|
@@ -1159,7 +1493,7 @@ async function edgeProxy(exec, opts = {}) {
|
|
|
1159
1493
|
return makeApi(exec, kind, ours, container);
|
|
1160
1494
|
}
|
|
1161
1495
|
function edgeProxyFor(exec, kind, opts = {}) {
|
|
1162
|
-
return makeApi(exec, kind, opts.ours ??
|
|
1496
|
+
return makeApi(exec, kind, opts.ours ?? false, opts.container ?? null);
|
|
1163
1497
|
}
|
|
1164
1498
|
async function collectProxyCerts(api, sites) {
|
|
1165
1499
|
const certPems = {};
|
|
@@ -1223,147 +1557,6 @@ function makeApi(exec, kind, ours, container) {
|
|
|
1223
1557
|
};
|
|
1224
1558
|
}
|
|
1225
1559
|
|
|
1226
|
-
// ../../packages/adapters/src/system/proxy/takeover-journal.ts
|
|
1227
|
-
var JOURNAL_DIR = "/var/lib/openship";
|
|
1228
|
-
var JOURNAL_PATH = `${JOURNAL_DIR}/edge-takeover.json`;
|
|
1229
|
-
function log(message, level = "info") {
|
|
1230
|
-
return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
|
|
1231
|
-
}
|
|
1232
|
-
async function tryExec2(executor, cmd) {
|
|
1233
|
-
try {
|
|
1234
|
-
return await executor.exec(cmd);
|
|
1235
|
-
} catch {
|
|
1236
|
-
return null;
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
async function buildJournal(executor, status) {
|
|
1240
|
-
const units = /* @__PURE__ */ new Map();
|
|
1241
|
-
const containers = /* @__PURE__ */ new Map();
|
|
1242
|
-
const processes = /* @__PURE__ */ new Map();
|
|
1243
|
-
for (const o of status.occupants) {
|
|
1244
|
-
if (o.containerName && !containers.has(o.containerName)) {
|
|
1245
|
-
const r = await tryExec2(
|
|
1246
|
-
executor,
|
|
1247
|
-
`docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' ${sq2(o.containerName)} 2>/dev/null`
|
|
1248
|
-
);
|
|
1249
|
-
containers.set(o.containerName, { name: o.containerName, restart: r?.trim() || "no" });
|
|
1250
|
-
} else if (o.systemdUnit && !units.has(o.systemdUnit)) {
|
|
1251
|
-
const en = await tryExec2(executor, `systemctl is-enabled ${sq2(o.systemdUnit)} 2>/dev/null`);
|
|
1252
|
-
units.set(o.systemdUnit, { unit: o.systemdUnit, wasEnabled: en?.trim() === "enabled" });
|
|
1253
|
-
} else if (!o.containerName && !o.systemdUnit && o.pid && !processes.has(o.pid)) {
|
|
1254
|
-
processes.set(o.pid, { pid: o.pid, command: o.rawCommand });
|
|
1255
|
-
}
|
|
1256
|
-
}
|
|
1257
|
-
return {
|
|
1258
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1259
|
-
units: [...units.values()],
|
|
1260
|
-
containers: [...containers.values()],
|
|
1261
|
-
processes: [...processes.values()]
|
|
1262
|
-
};
|
|
1263
|
-
}
|
|
1264
|
-
async function writeJournal(executor, journal) {
|
|
1265
|
-
try {
|
|
1266
|
-
await executor.mkdir(JOURNAL_DIR);
|
|
1267
|
-
await executor.writeFile(JOURNAL_PATH, JSON.stringify(journal, null, 2));
|
|
1268
|
-
} catch {
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
async function clearJournal(executor) {
|
|
1272
|
-
await tryExec2(executor, `rm -f ${JOURNAL_PATH}`);
|
|
1273
|
-
}
|
|
1274
|
-
async function readJournal(executor) {
|
|
1275
|
-
const raw = await tryExec2(executor, `cat ${JOURNAL_PATH} 2>/dev/null`);
|
|
1276
|
-
if (!raw?.trim()) return null;
|
|
1277
|
-
try {
|
|
1278
|
-
return JSON.parse(raw);
|
|
1279
|
-
} catch {
|
|
1280
|
-
await clearJournal(executor);
|
|
1281
|
-
return null;
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
async function rollback(executor, journal, onLog) {
|
|
1285
|
-
onLog(log("Rolling back \u2014 restoring the previous proxy...", "warn"));
|
|
1286
|
-
await tryExec2(executor, `docker update --restart=no ${sq2(EDGE_CONTAINER_NAME)} 2>/dev/null || true`);
|
|
1287
|
-
await tryExec2(executor, `docker stop ${sq2(EDGE_CONTAINER_NAME)} 2>/dev/null || true`);
|
|
1288
|
-
await tryExec2(
|
|
1289
|
-
executor,
|
|
1290
|
-
"systemctl disable --now openresty 2>/dev/null || systemctl stop openresty 2>/dev/null || true; systemctl reset-failed openresty 2>/dev/null || true"
|
|
1291
|
-
);
|
|
1292
|
-
for (const u of journal.units) {
|
|
1293
|
-
await tryExec2(
|
|
1294
|
-
executor,
|
|
1295
|
-
u.wasEnabled ? `systemctl enable --now ${sq2(u.unit)} 2>/dev/null || true` : `systemctl start ${sq2(u.unit)} 2>/dev/null || true`
|
|
1296
|
-
);
|
|
1297
|
-
}
|
|
1298
|
-
for (const c of journal.containers) {
|
|
1299
|
-
await tryExec2(executor, `docker update --restart=${sq2(c.restart)} ${sq2(c.name)} 2>/dev/null || true`);
|
|
1300
|
-
await tryExec2(executor, `docker start ${sq2(c.name)} 2>/dev/null || true`);
|
|
1301
|
-
}
|
|
1302
|
-
for (const p of journal.processes ?? []) {
|
|
1303
|
-
if (p.command) {
|
|
1304
|
-
await tryExec2(
|
|
1305
|
-
executor,
|
|
1306
|
-
`setsid -f sh -c ${sq2(p.command)} 2>/dev/null || (nohup sh -c ${sq2(p.command)} >/dev/null 2>&1 &) || true`
|
|
1307
|
-
);
|
|
1308
|
-
} else {
|
|
1309
|
-
onLog(log(`Could not restore process ${p.pid} \u2014 no command captured.`, "warn"));
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
|
-
const restored = await portIsServed(executor, 80);
|
|
1313
|
-
if (!restored) {
|
|
1314
|
-
onLog(
|
|
1315
|
-
log(
|
|
1316
|
-
"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>`).",
|
|
1317
|
-
"error"
|
|
1318
|
-
)
|
|
1319
|
-
);
|
|
1320
|
-
}
|
|
1321
|
-
return restored;
|
|
1322
|
-
}
|
|
1323
|
-
async function portIsServed(executor, port) {
|
|
1324
|
-
const hex = port.toString(16).toUpperCase().padStart(4, "0");
|
|
1325
|
-
const out = await tryExec2(
|
|
1326
|
-
executor,
|
|
1327
|
-
`(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`
|
|
1328
|
-
);
|
|
1329
|
-
return (out ?? "").includes("yes");
|
|
1330
|
-
}
|
|
1331
|
-
async function beginEdgeTakeover(executor, status, onLog) {
|
|
1332
|
-
await writeJournal(executor, await buildJournal(executor, status));
|
|
1333
|
-
await freeEdgeTargets(executor, stopTargetsForStatus(status), (m, l) => onLog(log(m, l)));
|
|
1334
|
-
}
|
|
1335
|
-
async function rollbackEdgeTakeover(executor, onLog) {
|
|
1336
|
-
const journal = await readJournal(executor);
|
|
1337
|
-
if (!journal || journal.completed) return false;
|
|
1338
|
-
const restored = await rollback(executor, journal, onLog);
|
|
1339
|
-
await clearJournal(executor);
|
|
1340
|
-
return restored;
|
|
1341
|
-
}
|
|
1342
|
-
async function completeEdgeTakeover(executor) {
|
|
1343
|
-
const journal = await readJournal(executor);
|
|
1344
|
-
if (journal) {
|
|
1345
|
-
journal.completed = true;
|
|
1346
|
-
await writeJournal(executor, journal);
|
|
1347
|
-
}
|
|
1348
|
-
await clearJournal(executor);
|
|
1349
|
-
}
|
|
1350
|
-
async function recoverInterruptedTakeover(executor, onLog, isEdgeHealthy) {
|
|
1351
|
-
const journal = await readJournal(executor);
|
|
1352
|
-
if (!journal) return;
|
|
1353
|
-
if (journal.completed) {
|
|
1354
|
-
await clearJournal(executor);
|
|
1355
|
-
return;
|
|
1356
|
-
}
|
|
1357
|
-
if (isEdgeHealthy && await isEdgeHealthy()) {
|
|
1358
|
-
onLog(log("Found an unfinished edge takeover, but our edge is serving \u2014 keeping it.", "warn"));
|
|
1359
|
-
await clearJournal(executor);
|
|
1360
|
-
return;
|
|
1361
|
-
}
|
|
1362
|
-
onLog(log("Found an interrupted edge takeover \u2014 restoring the previous proxy.", "warn"));
|
|
1363
|
-
await rollback(executor, journal, onLog);
|
|
1364
|
-
await clearJournal(executor);
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
1560
|
// ../../packages/adapters/src/system/catalog.ts
|
|
1368
1561
|
function dockerInstallPlan(profile) {
|
|
1369
1562
|
if (profile.os !== "linux") {
|
|
@@ -1421,120 +1614,15 @@ function rsyncInstallPlan(profile) {
|
|
|
1421
1614
|
verifyCommand: "rsync --version | head -n 1"
|
|
1422
1615
|
};
|
|
1423
1616
|
}
|
|
1424
|
-
var OPENRESTY_APT_SOURCES = [
|
|
1425
|
-
// ARM64: openresty.org publishes aarch64 debs under a SEPARATE path
|
|
1426
|
-
// (openresty.org/package/arm64/<repo>); the default path is amd64-only, so on
|
|
1427
|
-
// aarch64 `apt-get install openresty` finds no candidate and `set -e` aborts
|
|
1428
|
-
// the whole setup (issue #109). Switch the base URL by arch and tag the deb
|
|
1429
|
-
// line with `arch=` so apt fetches the right index. `dpkg --print-architecture`
|
|
1430
|
-
// returns exactly amd64/arm64 (matches OpenResty's path + arch qualifier).
|
|
1431
|
-
'OR_ARCH="$(dpkg --print-architecture 2>/dev/null || echo amd64)"',
|
|
1432
|
-
'if [ "$OR_ARCH" = arm64 ]; then OR_BASE="http://openresty.org/package/arm64/$REPO"; else OR_BASE="http://openresty.org/package/$REPO"; fi',
|
|
1433
|
-
// `|| REPO_CODENAME=""` so a failed substitution (no lsb_release AND no
|
|
1434
|
-
// /etc/os-release, e.g. a stripped container) doesn't trip `set -e` before
|
|
1435
|
-
// the empty-var fallback below can pick a default.
|
|
1436
|
-
'REPO_CODENAME="$(lsb_release -sc 2>/dev/null || { . /etc/os-release 2>/dev/null && echo "$VERSION_CODENAME"; })" || REPO_CODENAME=""',
|
|
1437
|
-
'if [ "$REPO" = debian ]; then OR_FALLBACKS="bookworm bullseye"; else OR_FALLBACKS="noble jammy focal"; fi',
|
|
1438
|
-
'OR_CODENAME=""',
|
|
1439
|
-
"for c in $REPO_CODENAME $OR_FALLBACKS; do",
|
|
1440
|
-
' if wget -q --spider --tries=2 --timeout=15 "$OR_BASE/dists/$c/Release"; then OR_CODENAME="$c"; break; fi',
|
|
1441
|
-
"done",
|
|
1442
|
-
'if [ -z "$OR_CODENAME" ]; then case "$REPO" in debian) OR_CODENAME=bookworm ;; *) OR_CODENAME=noble ;; esac; fi',
|
|
1443
|
-
'[ "$OR_CODENAME" = "$REPO_CODENAME" ] || echo "[openresty] apt repo has no codename $REPO_CODENAME; using nearest supported LTS $OR_CODENAME" >&2',
|
|
1444
|
-
'echo "deb [arch=$OR_ARCH signed-by=/usr/share/keyrings/openresty.gpg] $OR_BASE $OR_CODENAME main" > /etc/apt/sources.list.d/openresty.list'
|
|
1445
|
-
];
|
|
1446
|
-
function openrestyInstallPlan(profile) {
|
|
1447
|
-
if (profile.os !== "linux") {
|
|
1448
|
-
return {
|
|
1449
|
-
supported: false,
|
|
1450
|
-
unsupportedReason: "OpenResty installation is only supported on Linux servers"
|
|
1451
|
-
};
|
|
1452
|
-
}
|
|
1453
|
-
let installCommand;
|
|
1454
|
-
if (profile.packageManager === "apt") {
|
|
1455
|
-
const distro = profile.distro === "debian" ? "debian" : "ubuntu";
|
|
1456
|
-
installCommand = [
|
|
1457
|
-
"set -e",
|
|
1458
|
-
// Heal a stale/bad openresty.list left by a prior run (e.g. the #86
|
|
1459
|
-
// 'resolute' pin) BEFORE the first apt-get update reads it — otherwise
|
|
1460
|
-
// set -e aborts here and the repo never gets rewritten.
|
|
1461
|
-
"rm -f /etc/apt/sources.list.d/openresty.list",
|
|
1462
|
-
"apt-get update -qq && apt-get install -y -qq wget gnupg2 lsb-release",
|
|
1463
|
-
"wget -qO /tmp/openresty-pubkey.gpg https://openresty.org/package/pubkey.gpg",
|
|
1464
|
-
"gpg --yes --dearmor -o /usr/share/keyrings/openresty.gpg /tmp/openresty-pubkey.gpg",
|
|
1465
|
-
`REPO=${distro}`,
|
|
1466
|
-
...OPENRESTY_APT_SOURCES,
|
|
1467
|
-
"apt-get update -qq && apt-get install -y -qq openresty"
|
|
1468
|
-
].join("\n");
|
|
1469
|
-
} else if (profile.packageManager === "dnf") {
|
|
1470
|
-
const distro = profile.distro === "fedora" ? "fedora" : "centos";
|
|
1471
|
-
installCommand = `wget -qO /etc/yum.repos.d/openresty.repo https://openresty.org/package/${distro}/openresty.repo && dnf install -y openresty`;
|
|
1472
|
-
} else if (profile.packageManager === "yum") {
|
|
1473
|
-
installCommand = "wget -qO /etc/yum.repos.d/openresty.repo https://openresty.org/package/centos/openresty.repo && yum install -y openresty";
|
|
1474
|
-
} else if (profile.packageManager === "apk") {
|
|
1475
|
-
installCommand = [
|
|
1476
|
-
"apk add --no-cache wget",
|
|
1477
|
-
"wget -qO /etc/apk/keys/admin@openresty.com-5ea678a6.rsa.pub https://openresty.org/package/alpine/admin@openresty.com-5ea678a6.rsa.pub",
|
|
1478
|
-
`. /etc/os-release && echo "https://openresty.org/package/alpine/v$( echo $VERSION_ID | cut -d. -f1,2 )/main" >> /etc/apk/repositories`,
|
|
1479
|
-
"apk update && apk add openresty"
|
|
1480
|
-
].join(" && ");
|
|
1481
|
-
} else {
|
|
1482
|
-
installCommand = [
|
|
1483
|
-
"set -e",
|
|
1484
|
-
"if command -v apt-get >/dev/null 2>&1; then",
|
|
1485
|
-
" rm -f /etc/apt/sources.list.d/openresty.list",
|
|
1486
|
-
" apt-get update -qq && apt-get install -y -qq wget gnupg2 lsb-release",
|
|
1487
|
-
" wget -qO /tmp/openresty-pubkey.gpg https://openresty.org/package/pubkey.gpg",
|
|
1488
|
-
" gpg --yes --dearmor -o /usr/share/keyrings/openresty.gpg /tmp/openresty-pubkey.gpg",
|
|
1489
|
-
' DISTRO=$(. /etc/os-release 2>/dev/null && echo "$ID" || echo "ubuntu")',
|
|
1490
|
-
' case "$DISTRO" in debian) REPO=debian ;; *) REPO=ubuntu ;; esac',
|
|
1491
|
-
...OPENRESTY_APT_SOURCES.map((l) => " " + l),
|
|
1492
|
-
" apt-get update -qq && apt-get install -y -qq openresty",
|
|
1493
|
-
"elif command -v dnf >/dev/null 2>&1; then",
|
|
1494
|
-
" wget -qO /etc/yum.repos.d/openresty.repo https://openresty.org/package/centos/openresty.repo && dnf install -y openresty",
|
|
1495
|
-
"elif command -v yum >/dev/null 2>&1; then",
|
|
1496
|
-
" wget -qO /etc/yum.repos.d/openresty.repo https://openresty.org/package/centos/openresty.repo && yum install -y openresty",
|
|
1497
|
-
"elif command -v apk >/dev/null 2>&1; then",
|
|
1498
|
-
" apk add --no-cache wget \\",
|
|
1499
|
-
" && wget -qO /etc/apk/keys/admin@openresty.com-5ea678a6.rsa.pub https://openresty.org/package/alpine/admin@openresty.com-5ea678a6.rsa.pub \\",
|
|
1500
|
-
' && . /etc/os-release && echo "https://openresty.org/package/alpine/v$(echo $VERSION_ID | cut -d. -f1,2)/main" >> /etc/apk/repositories \\',
|
|
1501
|
-
" && apk update && apk add openresty",
|
|
1502
|
-
"else",
|
|
1503
|
-
' echo "No supported package manager found (tried apt-get, dnf, yum, apk)" >&2 && exit 1',
|
|
1504
|
-
"fi"
|
|
1505
|
-
].join("\n");
|
|
1506
|
-
}
|
|
1507
|
-
return {
|
|
1508
|
-
supported: true,
|
|
1509
|
-
installCommand,
|
|
1510
|
-
startCommand: profile.serviceManager === "systemd" ? "systemctl enable openresty && systemctl start openresty" : void 0,
|
|
1511
|
-
verifyCommand: "openresty -v 2>&1"
|
|
1512
|
-
};
|
|
1513
|
-
}
|
|
1514
|
-
function certbotInstallPlan(profile) {
|
|
1515
|
-
const commands = {
|
|
1516
|
-
apt: "apt-get update -qq && apt-get install -y -qq certbot",
|
|
1517
|
-
dnf: "dnf install -y certbot",
|
|
1518
|
-
yum: "yum install -y certbot",
|
|
1519
|
-
apk: "apk add --no-cache certbot"
|
|
1520
|
-
};
|
|
1521
|
-
const installCommand = commands[profile.packageManager];
|
|
1522
|
-
if (!installCommand) {
|
|
1523
|
-
return {
|
|
1524
|
-
supported: false,
|
|
1525
|
-
unsupportedReason: "Certbot installation is only supported on Linux with apt, dnf, or yum"
|
|
1526
|
-
};
|
|
1527
|
-
}
|
|
1528
|
-
return {
|
|
1529
|
-
supported: true,
|
|
1530
|
-
installCommand,
|
|
1531
|
-
verifyCommand: "certbot --version 2>/dev/null"
|
|
1532
|
-
};
|
|
1533
|
-
}
|
|
1534
1617
|
var systemCatalog = {
|
|
1535
1618
|
checks: {
|
|
1536
1619
|
docker: {
|
|
1537
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.
|
|
1538
1626
|
daemonCommand: "docker info --format '{{.ServerVersion}}'",
|
|
1539
1627
|
parseVersion: (output) => output.match(/Docker version ([^\s,]+)/)?.[1] ?? output,
|
|
1540
1628
|
missingMessage: "Docker is not installed",
|
|
@@ -1565,12 +1653,15 @@ var systemCatalog = {
|
|
|
1565
1653
|
missingMessage: "rsync is not installed"
|
|
1566
1654
|
}
|
|
1567
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`.
|
|
1568
1661
|
installs: {
|
|
1569
1662
|
docker: dockerInstallPlan,
|
|
1570
1663
|
git: gitInstallPlan,
|
|
1571
|
-
rsync: rsyncInstallPlan
|
|
1572
|
-
openresty: openrestyInstallPlan,
|
|
1573
|
-
certbot: certbotInstallPlan
|
|
1664
|
+
rsync: rsyncInstallPlan
|
|
1574
1665
|
}
|
|
1575
1666
|
};
|
|
1576
1667
|
|
|
@@ -1594,7 +1685,7 @@ function elevatedExecutor(inner) {
|
|
|
1594
1685
|
};
|
|
1595
1686
|
const overrides = {
|
|
1596
1687
|
exec: (command, opts) => inner.exec(elevateCommand(command), opts),
|
|
1597
|
-
streamExec: (command, onLog) => inner.streamExec(elevateCommand(command), onLog),
|
|
1688
|
+
streamExec: (command, onLog, opts) => inner.streamExec(elevateCommand(command), onLog, opts),
|
|
1598
1689
|
writeFile: writeFileElevated,
|
|
1599
1690
|
// Elevated for the same reason as writeFile: the vhost/conf dirs are root-owned,
|
|
1600
1691
|
// and the Proxy would otherwise forward this to the inner executor unelevated —
|
|
@@ -1621,7 +1712,7 @@ function elevatedExecutor(inner) {
|
|
|
1621
1712
|
}
|
|
1622
1713
|
|
|
1623
1714
|
// ../../packages/adapters/src/system/installer.ts
|
|
1624
|
-
function
|
|
1715
|
+
function log3(message, level = "info") {
|
|
1625
1716
|
return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
|
|
1626
1717
|
}
|
|
1627
1718
|
async function prepareExecutor(executor, component) {
|
|
@@ -1646,21 +1737,21 @@ async function installDocker(executor, onLog) {
|
|
|
1646
1737
|
if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
|
|
1647
1738
|
return { component: "docker", success: false, error: plan.unsupportedReason ?? "Docker install not supported" };
|
|
1648
1739
|
}
|
|
1649
|
-
onLog(
|
|
1740
|
+
onLog(log3("Installing Docker Engine..."));
|
|
1650
1741
|
try {
|
|
1651
1742
|
const { code } = await executor.streamExec(plan.installCommand, onLog);
|
|
1652
1743
|
if (code !== 0) return { component: "docker", success: false, error: "Docker install failed" };
|
|
1653
1744
|
if (plan.startCommand) {
|
|
1654
|
-
onLog(
|
|
1745
|
+
onLog(log3("Starting Docker service..."));
|
|
1655
1746
|
await executor.streamExec(plan.startCommand, onLog);
|
|
1656
1747
|
}
|
|
1657
1748
|
const version = await executor.exec(plan.verifyCommand);
|
|
1658
1749
|
const parsed = systemCatalog.checks.docker.parseVersion(version);
|
|
1659
|
-
onLog(
|
|
1750
|
+
onLog(log3(`Docker ${parsed} installed`));
|
|
1660
1751
|
return { component: "docker", success: true, version: parsed };
|
|
1661
1752
|
} catch (err) {
|
|
1662
1753
|
const msg = safeErrorMessage(err);
|
|
1663
|
-
onLog(
|
|
1754
|
+
onLog(log3(`Docker installation failed: ${msg}`, "error"));
|
|
1664
1755
|
return { component: "docker", success: false, error: msg };
|
|
1665
1756
|
}
|
|
1666
1757
|
}
|
|
@@ -1673,17 +1764,17 @@ async function installGit(executor, onLog, opts) {
|
|
|
1673
1764
|
if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
|
|
1674
1765
|
return { component: "git", success: false, error: plan.unsupportedReason ?? "Git install not supported" };
|
|
1675
1766
|
}
|
|
1676
|
-
onLog(
|
|
1767
|
+
onLog(log3(opts?.label ? `Installing Git in ${opts.label}...` : "Installing Git..."));
|
|
1677
1768
|
try {
|
|
1678
1769
|
const { code } = await executor.streamExec(plan.installCommand, onLog);
|
|
1679
1770
|
if (code !== 0) return { component: "git", success: false, error: "Git installation failed" };
|
|
1680
1771
|
const version = await executor.exec(plan.verifyCommand);
|
|
1681
1772
|
const parsed = systemCatalog.checks.git.parseVersion(version);
|
|
1682
|
-
onLog(
|
|
1773
|
+
onLog(log3(opts?.label ? `Git ${parsed} installed in ${opts.label}` : `Git ${parsed} installed`));
|
|
1683
1774
|
return { component: "git", success: true, version: parsed };
|
|
1684
1775
|
} catch (err) {
|
|
1685
1776
|
const msg = safeErrorMessage(err);
|
|
1686
|
-
onLog(
|
|
1777
|
+
onLog(log3(`Git installation failed: ${msg}`, "error"));
|
|
1687
1778
|
return { component: "git", success: false, error: msg };
|
|
1688
1779
|
}
|
|
1689
1780
|
}
|
|
@@ -1696,17 +1787,17 @@ async function installRsync(executor, onLog) {
|
|
|
1696
1787
|
if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
|
|
1697
1788
|
return { component: "rsync", success: false, error: plan.unsupportedReason ?? "rsync install not supported" };
|
|
1698
1789
|
}
|
|
1699
|
-
onLog(
|
|
1790
|
+
onLog(log3("Installing rsync..."));
|
|
1700
1791
|
try {
|
|
1701
1792
|
const { code } = await executor.streamExec(plan.installCommand, onLog);
|
|
1702
1793
|
if (code !== 0) return { component: "rsync", success: false, error: "rsync installation failed" };
|
|
1703
1794
|
const version = await executor.exec(plan.verifyCommand);
|
|
1704
1795
|
const parsed = systemCatalog.checks.rsync.parseVersion(version);
|
|
1705
|
-
onLog(
|
|
1796
|
+
onLog(log3(`rsync ${parsed} installed`));
|
|
1706
1797
|
return { component: "rsync", success: true, version: parsed };
|
|
1707
1798
|
} catch (err) {
|
|
1708
1799
|
const msg = safeErrorMessage(err);
|
|
1709
|
-
onLog(
|
|
1800
|
+
onLog(log3(`rsync installation failed: ${msg}`, "error"));
|
|
1710
1801
|
return { component: "rsync", success: false, error: msg };
|
|
1711
1802
|
}
|
|
1712
1803
|
}
|
|
@@ -1716,7 +1807,7 @@ async function installContainerEdge(executor, onLog, config) {
|
|
|
1716
1807
|
executor = prep.executor;
|
|
1717
1808
|
if (!await dockerAvailable(executor)) {
|
|
1718
1809
|
const error = "The edge is a container image and needs Docker on this server. Install the Docker component first, then install the edge.";
|
|
1719
|
-
onLog(
|
|
1810
|
+
onLog(log3(error, "error"));
|
|
1720
1811
|
return { component: "edge", success: false, error };
|
|
1721
1812
|
}
|
|
1722
1813
|
try {
|
|
@@ -1726,7 +1817,7 @@ async function installContainerEdge(executor, onLog, config) {
|
|
|
1726
1817
|
image: config?.edgeImage
|
|
1727
1818
|
});
|
|
1728
1819
|
if (result.converted) {
|
|
1729
|
-
onLog(
|
|
1820
|
+
onLog(log3("Edge moved from host OpenResty to the container edge", "warn"));
|
|
1730
1821
|
}
|
|
1731
1822
|
const version = await executor.exec(containerCommand(result.container, "openresty -v 2>&1")).catch(() => "");
|
|
1732
1823
|
return {
|
|
@@ -1737,18 +1828,17 @@ async function installContainerEdge(executor, onLog, config) {
|
|
|
1737
1828
|
} catch (err) {
|
|
1738
1829
|
if (err instanceof EdgeMigrateRequested) throw err;
|
|
1739
1830
|
const msg = safeErrorMessage(err);
|
|
1740
|
-
onLog(
|
|
1831
|
+
onLog(log3(`Edge setup failed: ${msg}`, "error"));
|
|
1741
1832
|
return { component: "edge", success: false, error: msg };
|
|
1742
1833
|
}
|
|
1743
1834
|
}
|
|
1744
1835
|
var COMPONENT_INSTALLERS = {
|
|
1745
|
-
docker: (exec,
|
|
1746
|
-
// The edge is ONE component and it is a container image
|
|
1747
|
-
//
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
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)
|
|
1752
1842
|
};
|
|
1753
1843
|
|
|
1754
1844
|
// ../../packages/adapters/src/system/available-version.ts
|
|
@@ -1876,7 +1966,7 @@ async function tryExec4(executor, command) {
|
|
|
1876
1966
|
"checks",
|
|
1877
1967
|
`exec:ok ${command} (${formatDuration(startedAt)})`
|
|
1878
1968
|
);
|
|
1879
|
-
return result;
|
|
1969
|
+
return { output: result.trim(), error: null };
|
|
1880
1970
|
} catch (err) {
|
|
1881
1971
|
if (isRemoteConnectionError(err)) {
|
|
1882
1972
|
systemDebug(
|
|
@@ -1890,9 +1980,15 @@ async function tryExec4(executor, command) {
|
|
|
1890
1980
|
"checks",
|
|
1891
1981
|
`exec:fail ${command} (${formatDuration(startedAt)}) ${msg}`
|
|
1892
1982
|
);
|
|
1893
|
-
return
|
|
1983
|
+
return { output: "", error: msg || `\`${command}\` failed` };
|
|
1894
1984
|
}
|
|
1895
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
|
+
}
|
|
1896
1992
|
function healthy(name, version, running) {
|
|
1897
1993
|
const component = getSystemComponentDefinition(name);
|
|
1898
1994
|
return {
|
|
@@ -1925,15 +2021,18 @@ async function checkDocker(executor) {
|
|
|
1925
2021
|
const startedAt = Date.now();
|
|
1926
2022
|
const recipe = systemCatalog.checks.docker;
|
|
1927
2023
|
const version = await tryExec4(executor, recipe.versionCommand);
|
|
1928
|
-
if (!version) {
|
|
2024
|
+
if (!version.output) {
|
|
1929
2025
|
systemDebug("checks", `docker:missing (${formatDuration(startedAt)})`);
|
|
1930
|
-
return unhealthy("docker", recipe.missingMessage);
|
|
2026
|
+
return unhealthy("docker", withReason(recipe.missingMessage, version.error));
|
|
1931
2027
|
}
|
|
1932
|
-
const parsed = recipe.parseVersion(version);
|
|
2028
|
+
const parsed = recipe.parseVersion(version.output);
|
|
1933
2029
|
const info = await tryExec4(executor, recipe.daemonCommand);
|
|
1934
|
-
if (!info) {
|
|
1935
|
-
systemDebug(
|
|
1936
|
-
|
|
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), {
|
|
1937
2036
|
version: parsed,
|
|
1938
2037
|
running: false
|
|
1939
2038
|
});
|
|
@@ -1945,11 +2044,11 @@ async function checkGit(executor) {
|
|
|
1945
2044
|
const startedAt = Date.now();
|
|
1946
2045
|
const recipe = systemCatalog.checks.git;
|
|
1947
2046
|
const version = await tryExec4(executor, recipe.versionCommand);
|
|
1948
|
-
if (!version) {
|
|
2047
|
+
if (!version.output) {
|
|
1949
2048
|
systemDebug("checks", `git:missing (${formatDuration(startedAt)})`);
|
|
1950
|
-
return unhealthy("git", recipe.missingMessage);
|
|
2049
|
+
return unhealthy("git", withReason(recipe.missingMessage, version.error));
|
|
1951
2050
|
}
|
|
1952
|
-
const parsed = recipe.parseVersion(version);
|
|
2051
|
+
const parsed = recipe.parseVersion(version.output);
|
|
1953
2052
|
systemDebug("checks", `git:healthy (${formatDuration(startedAt)})`);
|
|
1954
2053
|
return healthy("git", parsed);
|
|
1955
2054
|
}
|
|
@@ -1957,11 +2056,11 @@ async function checkRsync(executor) {
|
|
|
1957
2056
|
const startedAt = Date.now();
|
|
1958
2057
|
const recipe = systemCatalog.checks.rsync;
|
|
1959
2058
|
const version = await tryExec4(executor, recipe.versionCommand);
|
|
1960
|
-
if (!version) {
|
|
2059
|
+
if (!version.output) {
|
|
1961
2060
|
systemDebug("checks", `rsync:missing (${formatDuration(startedAt)})`);
|
|
1962
|
-
return unhealthy("rsync", recipe.missingMessage);
|
|
2061
|
+
return unhealthy("rsync", withReason(recipe.missingMessage, version.error));
|
|
1963
2062
|
}
|
|
1964
|
-
const parsed = recipe.parseVersion(version);
|
|
2063
|
+
const parsed = recipe.parseVersion(version.output);
|
|
1965
2064
|
systemDebug("checks", `rsync:healthy (${formatDuration(startedAt)})`);
|
|
1966
2065
|
return healthy("rsync", parsed);
|
|
1967
2066
|
}
|
|
@@ -1975,17 +2074,29 @@ async function checkEdge(executor) {
|
|
|
1975
2074
|
"No edge on this server. The edge is the openship-edge container \u2014 install it (requires Docker)."
|
|
1976
2075
|
);
|
|
1977
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
|
+
}
|
|
1978
2086
|
const version = await tryExec4(executor, containerCommand(container, "openresty -v 2>&1"));
|
|
1979
|
-
if (!version) {
|
|
2087
|
+
if (!version.output) {
|
|
1980
2088
|
systemDebug("checks", `edge:container-unresponsive (${formatDuration(startedAt)})`);
|
|
1981
2089
|
return unhealthy(
|
|
1982
2090
|
"edge",
|
|
1983
|
-
|
|
2091
|
+
withReason(
|
|
2092
|
+
`The edge container ${container} is serving on :80 but not answering \u2014 check \`docker logs ${container}\``,
|
|
2093
|
+
version.error
|
|
2094
|
+
),
|
|
1984
2095
|
{ running: false }
|
|
1985
2096
|
);
|
|
1986
2097
|
}
|
|
1987
2098
|
systemDebug("checks", `edge:healthy (${formatDuration(startedAt)})`);
|
|
1988
|
-
return healthy("edge", systemCatalog.checks.openresty.parseVersion(version), true);
|
|
2099
|
+
return healthy("edge", systemCatalog.checks.openresty.parseVersion(version.output), true);
|
|
1989
2100
|
}
|
|
1990
2101
|
var COMPONENT_CHECKS = {
|
|
1991
2102
|
docker: checkDocker,
|
|
@@ -2075,24 +2186,36 @@ function describeEdgeOwner(occupants) {
|
|
|
2075
2186
|
}
|
|
2076
2187
|
|
|
2077
2188
|
// ../../packages/adapters/src/system/proxy/consent.ts
|
|
2078
|
-
function
|
|
2189
|
+
function log4(message, level = "info") {
|
|
2079
2190
|
return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
|
|
2080
2191
|
}
|
|
2081
2192
|
async function ensureEdgeClear(executor, config, onLog) {
|
|
2082
2193
|
const status = await probeEdge(executor);
|
|
2083
|
-
if (status.canProceedClean) return { tookOver: false };
|
|
2194
|
+
if (status.canProceedClean) return { tookOver: false, status };
|
|
2084
2195
|
const takeover = async () => {
|
|
2085
|
-
onLog(
|
|
2196
|
+
onLog(log4(
|
|
2086
2197
|
`Taking over ports from ${status.occupants.map((o) => o.command ?? o.port).join(", ")}...`,
|
|
2087
2198
|
"warn"
|
|
2088
2199
|
));
|
|
2089
|
-
const
|
|
2090
|
-
|
|
2091
|
-
|
|
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
|
+
}
|
|
2092
2215
|
};
|
|
2093
2216
|
if (config?.edgePolicy?.mode === "takeover") {
|
|
2094
2217
|
await takeover();
|
|
2095
|
-
return { tookOver: true };
|
|
2218
|
+
return { tookOver: true, status };
|
|
2096
2219
|
}
|
|
2097
2220
|
if (config?.promptUser) {
|
|
2098
2221
|
const known = status.classification === "known";
|
|
@@ -2121,7 +2244,7 @@ async function ensureEdgeClear(executor, config, onLog) {
|
|
|
2121
2244
|
}
|
|
2122
2245
|
if (action === "override") {
|
|
2123
2246
|
await takeover();
|
|
2124
|
-
return { tookOver: true };
|
|
2247
|
+
return { tookOver: true, status };
|
|
2125
2248
|
}
|
|
2126
2249
|
throw new EdgeConflictError(status);
|
|
2127
2250
|
}
|
|
@@ -2129,7 +2252,7 @@ async function ensureEdgeClear(executor, config, onLog) {
|
|
|
2129
2252
|
}
|
|
2130
2253
|
|
|
2131
2254
|
// ../../packages/adapters/src/system/proxy/ensure-container-edge.ts
|
|
2132
|
-
function
|
|
2255
|
+
function log5(message, level = "info") {
|
|
2133
2256
|
return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
|
|
2134
2257
|
}
|
|
2135
2258
|
var injectedDefaultImage;
|
|
@@ -2139,15 +2262,8 @@ function setDefaultEdgeImage(image) {
|
|
|
2139
2262
|
function resolveEdgeImage(explicit) {
|
|
2140
2263
|
return buildEdgeImageRef({ explicit, injectedDefault: injectedDefaultImage });
|
|
2141
2264
|
}
|
|
2142
|
-
async function dockerAvailable(executor) {
|
|
2143
|
-
return executor.exec("docker version --format '{{.Server.Version}}' 2>/dev/null").then((v) => Boolean(v.trim())).catch(() => false);
|
|
2144
|
-
}
|
|
2145
|
-
async function runningImage(executor, container) {
|
|
2146
|
-
const out = await executor.exec(`docker inspect -f '{{.Config.Image}}' ${sq(container)} 2>/dev/null`).catch(() => "");
|
|
2147
|
-
return out.trim() || null;
|
|
2148
|
-
}
|
|
2149
2265
|
async function containerEdgeProvider(executor, container, opts) {
|
|
2150
|
-
const { NginxProvider: Provider } = await import("./nginx-
|
|
2266
|
+
const { NginxProvider: Provider } = await import("./nginx-AXGNWRKZ.js");
|
|
2151
2267
|
return new Provider({
|
|
2152
2268
|
...opts,
|
|
2153
2269
|
// Vhosts go to the BIND-MOUNTED HOST dir (so every host-side reader — the
|
|
@@ -2155,6 +2271,13 @@ async function containerEdgeProvider(executor, container, opts) {
|
|
|
2155
2271
|
// while reload/certbot run inside the container.
|
|
2156
2272
|
paths: EDGE_HOST_PATHS,
|
|
2157
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,
|
|
2158
2281
|
// MUST be pinned: re-detection answers from inside the container and would
|
|
2159
2282
|
// repoint sitesDir at a host dir the edge never reads.
|
|
2160
2283
|
pinPaths: true,
|
|
@@ -2164,8 +2287,8 @@ async function containerEdgeProvider(executor, container, opts) {
|
|
|
2164
2287
|
});
|
|
2165
2288
|
}
|
|
2166
2289
|
async function localContainerEdgeProvider(container, opts) {
|
|
2167
|
-
const { DockerEdgeExecutor } = await import("./docker-edge-executor-
|
|
2168
|
-
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");
|
|
2169
2292
|
return new Provider({
|
|
2170
2293
|
...opts,
|
|
2171
2294
|
paths: OPENRESTY_DEFAULT_PATHS,
|
|
@@ -2203,37 +2326,39 @@ function buildEdgeRunCommand(container, image) {
|
|
|
2203
2326
|
sq(image)
|
|
2204
2327
|
].join(" ");
|
|
2205
2328
|
}
|
|
2206
|
-
|
|
2329
|
+
function makeEdgeStart(executor, container, opts) {
|
|
2207
2330
|
const { onLog } = opts;
|
|
2208
|
-
|
|
2209
|
-
const pull = await executor.streamExec(`docker pull ${sq(to)}`, onLog);
|
|
2210
|
-
if (pull.code !== 0) {
|
|
2211
|
-
onLog(log4(`Could not pull ${to} \u2014 the edge stays on ${from}.`, "warn"));
|
|
2212
|
-
return { swapped: false, edgeDown: false };
|
|
2213
|
-
}
|
|
2214
|
-
const start = async (image) => {
|
|
2331
|
+
return async (image) => {
|
|
2215
2332
|
if (!await startEdgeContainer(executor, container, image, onLog)) return false;
|
|
2216
|
-
const
|
|
2333
|
+
const verdict = await verifyEdgeServing(executor, container, {
|
|
2217
2334
|
timeoutMs: opts.verifyTimeoutMs ?? 3e4
|
|
2218
2335
|
});
|
|
2219
|
-
|
|
2336
|
+
if (!verdict.serving && verdict.reason) onLog(log5(`Edge ${image}: ${verdict.reason}`, "warn"));
|
|
2337
|
+
return verdict.serving;
|
|
2220
2338
|
};
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
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" };
|
|
2224
2344
|
}
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
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
|
+
}
|
|
2228
2352
|
}
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
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 };
|
|
2237
2362
|
}
|
|
2238
2363
|
async function ensureContainerEdge(executor, opts) {
|
|
2239
2364
|
const { onLog } = opts;
|
|
@@ -2241,123 +2366,112 @@ async function ensureContainerEdge(executor, opts) {
|
|
|
2241
2366
|
const image = resolveEdgeImage(opts.image);
|
|
2242
2367
|
const existing = await resolveOurEdgeContainer(executor, { fresh: true });
|
|
2243
2368
|
if (existing) {
|
|
2244
|
-
const
|
|
2245
|
-
if (
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
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 };
|
|
2254
2395
|
}
|
|
2255
|
-
return { container: existing, image, converted: false };
|
|
2256
2396
|
}
|
|
2257
|
-
await ensureEdgeClear(executor, opts.config, onLog);
|
|
2258
2397
|
if (!await dockerAvailable(executor)) {
|
|
2259
2398
|
throw new Error(
|
|
2260
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."
|
|
2261
2400
|
);
|
|
2262
2401
|
}
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
)
|
|
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
|
+
}
|
|
2270
2413
|
}
|
|
2271
2414
|
for (const mount of EDGE_CONTAINER_MOUNTS) {
|
|
2272
2415
|
await executor.exec(`mkdir -p ${sq(mount.host)}`).catch(() => {
|
|
2273
2416
|
});
|
|
2274
2417
|
}
|
|
2275
|
-
const
|
|
2276
|
-
let beforeCarry = null;
|
|
2277
|
-
if (bareWasOurs) {
|
|
2278
|
-
const barePaths = await detectOpenRestyPaths(executor).catch(() => OPENRESTY_DEFAULT_PATHS);
|
|
2279
|
-
if (barePaths.sitesDir !== sitesTarget && await executor.exists(barePaths.sitesDir)) {
|
|
2280
|
-
onLog(log4(`Carrying vhosts from ${barePaths.sitesDir}...`));
|
|
2281
|
-
beforeCarry = await listDir(executor, sitesTarget);
|
|
2282
|
-
await executor.exec(`cp -a ${sq(`${barePaths.sitesDir}/.`)} ${sq(`${sitesTarget}/`)} 2>/dev/null || true`).catch(() => {
|
|
2283
|
-
});
|
|
2284
|
-
}
|
|
2285
|
-
}
|
|
2286
|
-
const restoreBare = async () => {
|
|
2287
|
-
if (!bareWasOurs) return;
|
|
2288
|
-
if (beforeCarry) {
|
|
2289
|
-
const now = await listDir(executor, sitesTarget);
|
|
2290
|
-
const added = [...now].filter((name) => !beforeCarry.has(name));
|
|
2291
|
-
if (added.length > 0) {
|
|
2292
|
-
await executor.exec(`rm -f ${added.map((n) => sq(`${sitesTarget}/${n}`)).join(" ")}`).catch(() => {
|
|
2293
|
-
});
|
|
2294
|
-
onLog(log4(`Removed ${added.length} carried vhost(s) so the next edge start is clean.`, "warn"));
|
|
2295
|
-
}
|
|
2296
|
-
}
|
|
2297
|
-
onLog(log4("Restoring the host OpenResty edge...", "warn"));
|
|
2298
|
-
await executor.exec("systemctl enable --now openresty 2>/dev/null || true").catch(() => {
|
|
2299
|
-
});
|
|
2300
|
-
};
|
|
2418
|
+
const clear = await ensureEdgeClear(executor, opts.config, onLog);
|
|
2301
2419
|
try {
|
|
2302
|
-
|
|
2303
|
-
onLog(log4("Stopping the host OpenResty edge..."));
|
|
2304
|
-
await executor.exec("systemctl disable --now openresty 2>/dev/null || true").catch(() => {
|
|
2305
|
-
});
|
|
2306
|
-
await executor.exec("systemctl reset-failed openresty 2>/dev/null || true").catch(() => {
|
|
2307
|
-
});
|
|
2308
|
-
}
|
|
2309
|
-
onLog(log4("Starting the edge container..."));
|
|
2420
|
+
onLog(log5("Starting the edge container..."));
|
|
2310
2421
|
if (!await startEdgeContainer(executor, container, image, onLog)) {
|
|
2311
2422
|
throw new Error("the edge container failed to start");
|
|
2312
2423
|
}
|
|
2313
|
-
await executor
|
|
2314
|
-
|
|
2424
|
+
const verdict = await verifyEdgeServing(executor, container, {
|
|
2425
|
+
configTest: true,
|
|
2315
2426
|
timeoutMs: opts.verifyTimeoutMs ?? 3e4
|
|
2316
2427
|
});
|
|
2317
|
-
if (
|
|
2318
|
-
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");
|
|
2319
2430
|
}
|
|
2431
|
+
if (clear.tookOver) await completeEdgeTakeover(executor);
|
|
2320
2432
|
const version = await executor.exec(containerCommand(container, "openresty -v 2>&1")).catch(() => "");
|
|
2321
|
-
onLog(
|
|
2322
|
-
return { container, image, converted:
|
|
2433
|
+
onLog(log5(`Edge container running${version.trim() ? ` (${version.trim()})` : ""}`));
|
|
2434
|
+
return { container, image, converted: clear.tookOver };
|
|
2323
2435
|
} catch (err) {
|
|
2324
2436
|
const msg = safeErrorMessage(err);
|
|
2325
|
-
onLog(
|
|
2437
|
+
onLog(log5(`Edge container setup failed: ${msg}`, "error"));
|
|
2326
2438
|
const logs = await executor.exec(`docker logs --tail 40 ${sq(container)} 2>&1 || true`).catch(() => "");
|
|
2327
|
-
if (logs.trim()) onLog(
|
|
2439
|
+
if (logs.trim()) onLog(log5(`Edge container logs:
|
|
2328
2440
|
${logs}`, "error"));
|
|
2329
2441
|
await executor.exec(`docker rm -f ${sq(container)} 2>/dev/null || true`).catch(() => {
|
|
2330
2442
|
});
|
|
2331
2443
|
invalidateEdgeContainer(executor);
|
|
2332
|
-
await
|
|
2444
|
+
if (clear.tookOver) await rollbackEdgeTakeover(executor, onLog);
|
|
2333
2445
|
const reason = edgeFailureReason(logs);
|
|
2334
2446
|
throw new Error(`Edge container setup failed: ${reason ? `${reason} (${msg})` : msg}`);
|
|
2335
2447
|
}
|
|
2336
2448
|
}
|
|
2337
2449
|
|
|
2338
2450
|
export {
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2451
|
+
beginEdgeTakeover,
|
|
2452
|
+
rollbackEdgeTakeover,
|
|
2453
|
+
completeEdgeTakeover,
|
|
2454
|
+
recoverInterruptedTakeover,
|
|
2343
2455
|
detectInstalledProxy,
|
|
2344
2456
|
scanImportableSites,
|
|
2457
|
+
unreachableStaticRoots,
|
|
2458
|
+
copyStaticRootIntoEdge,
|
|
2345
2459
|
edgeProxy,
|
|
2346
2460
|
edgeProxyFor,
|
|
2347
2461
|
collectProxyCerts,
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2462
|
+
systemCatalog,
|
|
2463
|
+
installGit,
|
|
2464
|
+
COMPONENT_INSTALLERS,
|
|
2465
|
+
importSites,
|
|
2466
|
+
foreignProxyOnEdge,
|
|
2352
2467
|
setDefaultEdgeImage,
|
|
2353
2468
|
resolveEdgeImage,
|
|
2354
|
-
dockerAvailable,
|
|
2355
2469
|
containerEdgeProvider,
|
|
2356
2470
|
localContainerEdgeProvider,
|
|
2357
2471
|
buildEdgeRunCommand,
|
|
2472
|
+
verifyEdgeServing,
|
|
2358
2473
|
ensureContainerEdge,
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
COMPONENT_INSTALLERS
|
|
2474
|
+
checkGit,
|
|
2475
|
+
checkAll,
|
|
2476
|
+
checkComponents
|
|
2363
2477
|
};
|