openship 0.6.0 → 0.6.5
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 +7 -2
- package/dist/{bare-FSUAM5V5.js → bare-XQIPDO4R.js} +10 -9
- package/dist/{chunk-PSZSKEPA.js → chunk-3ZAGC5NT.js} +64 -17
- package/dist/{chunk-6ODUBKUB.js → chunk-5OUWIPOB.js} +69 -7
- package/dist/chunk-6HOA5JT7.js +124 -0
- package/dist/{chunk-TWJCLD4W.js → chunk-6JELW75A.js} +4 -4
- package/dist/{chunk-GUQ3QCJ7.js → chunk-7334NSJL.js} +399 -369
- package/dist/chunk-7S4USQYF.js +1753 -0
- package/dist/{chunk-3LDWWY7U.js → chunk-A7EWM2KA.js} +41 -7
- package/dist/chunk-ELGRMZOW.js +25 -0
- package/dist/chunk-EZPPTWJQ.js +292 -0
- package/dist/{chunk-IXM5245F.js → chunk-GRTXMUHA.js} +553 -538
- package/dist/chunk-HPXP6LXZ.js +428 -0
- package/dist/{chunk-OTLSJYAC.js → chunk-LEYSL46B.js} +2135 -85
- package/dist/{chunk-EMXI4UCU.js → chunk-LFCSC4IU.js} +29 -10
- package/dist/{chunk-JIMUYEKN.js → chunk-MSZUCNPV.js} +1 -1
- package/dist/{chunk-OGY63DGD.js → chunk-PZRRL5PC.js} +71 -28
- package/dist/{chunk-VE2YOK4I.js → chunk-QLX7NBEA.js} +1218 -588
- package/dist/chunk-RFWJE7ZN.js +68 -0
- package/dist/{chunk-VDDMADOF.js → chunk-TM2IQPYU.js} +16 -2
- package/dist/{chunk-3FYIXVX5.js → chunk-VDESJSG6.js} +451 -106
- package/dist/{chunk-B3CE2OMR.js → chunk-VHMOQL5Y.js} +276 -50
- package/dist/{chunk-QEH4QUBX.js → chunk-VLQ3UIVO.js} +393 -53
- package/dist/{chunk-VZ2ZHPS2.js → chunk-WE7BXW6L.js} +3 -9
- package/dist/chunk-WFRXYWK2.js +444 -0
- package/dist/chunk-XRNRT37W.js +316 -0
- package/dist/{chunk-EP2TNALB.js → chunk-XVOBL7BC.js} +31 -8
- package/dist/cloud-3DODOWTR.js +36 -0
- package/dist/{detect-ALF4GXRX.js → detect-P4CIQBRH.js} +3 -3
- package/dist/{docker-QECQJ5P4.js → docker-3K2NEBD6.js} +15 -8
- package/dist/{docker-edge-executor-62N64MGS.js → docker-edge-executor-YZZ6P7IC.js} +5 -5
- package/dist/{edge-import-CQWHN2Q3.js → edge-import-MCJNVTZ2.js} +26 -23
- package/dist/edge-preflight-ES6FH4JK.js +42 -0
- package/dist/{ensure-container-edge-MJVXJQG5.js → ensure-container-edge-YPITNH45.js} +9 -8
- package/dist/{executor-JELARVKC.js → executor-DZGX55K2.js} +12 -5
- package/dist/host-channel-preflight-MUXE3KZU.js +49 -0
- package/dist/index.js +3668 -4063
- package/dist/{nginx-AXGNWRKZ.js → nginx-NSAHWGJV.js} +11 -5
- package/dist/node-bootstrap.js +241 -0
- package/dist/node-entry.js +52 -1
- package/dist/{openresty-lua-IE4P5YQX.js → openresty-lua-DR4D3EZ6.js} +6 -2
- package/dist/privilege-YTVI4SKF.js +19 -0
- package/dist/server/engine/samples/fail2ban/jail.d/sshd.local +4 -1
- package/dist/server/index.js +21501 -13137
- package/dist/server/lua/geo_country.lua +4 -3
- package/dist/server/lua/pipe_stream.lua +52 -39
- package/dist/server/lua/site_logger.lua +22 -24
- package/dist/server/migrations/0098_instance_product_mode.sql +12 -0
- package/dist/server/migrations/0099_mail_webmail_project.sql +24 -0
- package/dist/server/migrations/0100_server_ssh_private_key.sql +11 -0
- package/dist/server/migrations/0101_mail_resume_step.sql +12 -0
- package/dist/server/migrations/0102_project_workload_axes.sql +19 -0
- package/dist/server/migrations/0103_dns_credential.sql +41 -0
- package/dist/server/migrations/0104_instance_host_control.sql +15 -0
- package/dist/server/migrations/meta/_journal.json +49 -0
- package/dist/{setup-FA4PW4NU.js → setup-WUUZURHS.js} +10 -9
- package/dist/up-XEO3SR6Z.js +58 -0
- package/package.json +4 -2
- package/dist/chunk-IW2UKBVF.js +0 -144
- package/dist/chunk-ZCFVHA25.js +0 -89
- package/dist/cloud-E5M6VMCG.js +0 -31
- package/dist/server/lua/pipe_log.lua +0 -77
- /package/dist/{dist-es-JGBPABJI.js → dist-es-ZP5QDSJ3.js} +0 -0
|
@@ -2,17 +2,32 @@
|
|
|
2
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
|
-
import {
|
|
6
|
-
ENV_KEY,
|
|
7
|
-
ENV_SECRET,
|
|
8
|
-
fromEnv
|
|
9
|
-
} from "./chunk-XZYFZ6A5.js";
|
|
10
5
|
import {
|
|
11
6
|
SignatureV4MultiRegion,
|
|
12
7
|
XmlNode,
|
|
13
8
|
XmlShapeDeserializer,
|
|
14
9
|
XmlText
|
|
15
10
|
} from "./chunk-QWHBRY2K.js";
|
|
11
|
+
import {
|
|
12
|
+
OS_DIR
|
|
13
|
+
} from "./chunk-ELGRMZOW.js";
|
|
14
|
+
import {
|
|
15
|
+
AwsCrc32,
|
|
16
|
+
__awaiter,
|
|
17
|
+
__generator,
|
|
18
|
+
__values,
|
|
19
|
+
convertToBuffer,
|
|
20
|
+
eventStreamSerdeProvider2 as eventStreamSerdeProvider,
|
|
21
|
+
isEmptyData,
|
|
22
|
+
numToUint8,
|
|
23
|
+
resolveEventStreamSerdeConfig,
|
|
24
|
+
uint32ArrayFrom
|
|
25
|
+
} from "./chunk-ONKEYSMY.js";
|
|
26
|
+
import {
|
|
27
|
+
ENV_KEY,
|
|
28
|
+
ENV_SECRET,
|
|
29
|
+
fromEnv
|
|
30
|
+
} from "./chunk-XZYFZ6A5.js";
|
|
16
31
|
import {
|
|
17
32
|
AwsSdkSigV4ASigner,
|
|
18
33
|
AwsSdkSigV4Signer,
|
|
@@ -99,24 +114,6 @@ import {
|
|
|
99
114
|
resolveRegionConfig,
|
|
100
115
|
toEndpointV1
|
|
101
116
|
} from "./chunk-3NQQXMDR.js";
|
|
102
|
-
import {
|
|
103
|
-
CloudRuntime
|
|
104
|
-
} from "./chunk-PSZSKEPA.js";
|
|
105
|
-
import {
|
|
106
|
-
systemCatalog
|
|
107
|
-
} from "./chunk-GUQ3QCJ7.js";
|
|
108
|
-
import {
|
|
109
|
-
AwsCrc32,
|
|
110
|
-
__awaiter,
|
|
111
|
-
__generator,
|
|
112
|
-
__values,
|
|
113
|
-
convertToBuffer,
|
|
114
|
-
eventStreamSerdeProvider2 as eventStreamSerdeProvider,
|
|
115
|
-
isEmptyData,
|
|
116
|
-
numToUint8,
|
|
117
|
-
resolveEventStreamSerdeConfig,
|
|
118
|
-
uint32ArrayFrom
|
|
119
|
-
} from "./chunk-ONKEYSMY.js";
|
|
120
117
|
import {
|
|
121
118
|
Hash,
|
|
122
119
|
NumericValue,
|
|
@@ -136,41 +133,232 @@ import {
|
|
|
136
133
|
toUtf8
|
|
137
134
|
} from "./chunk-ZWLDLTJP.js";
|
|
138
135
|
import {
|
|
136
|
+
CloudRuntime
|
|
137
|
+
} from "./chunk-3ZAGC5NT.js";
|
|
138
|
+
import {
|
|
139
|
+
systemCatalog
|
|
140
|
+
} from "./chunk-7334NSJL.js";
|
|
141
|
+
import {
|
|
142
|
+
DEFAULT_DOCKER_SOCKET_PATH,
|
|
139
143
|
DockerRuntime,
|
|
140
144
|
daemonConnectionFrom,
|
|
141
145
|
isHostPathSource,
|
|
142
146
|
resolveExecExitCode,
|
|
147
|
+
resolveLocalDockerSocketPath,
|
|
143
148
|
scopedVolumeName,
|
|
144
149
|
startAttachStream,
|
|
145
150
|
startExecStream
|
|
146
|
-
} from "./chunk-
|
|
151
|
+
} from "./chunk-VDESJSG6.js";
|
|
147
152
|
import {
|
|
148
153
|
BareRuntime
|
|
149
|
-
} from "./chunk-
|
|
154
|
+
} from "./chunk-GRTXMUHA.js";
|
|
155
|
+
import {
|
|
156
|
+
createExecutor
|
|
157
|
+
} from "./chunk-VHMOQL5Y.js";
|
|
150
158
|
import {
|
|
151
159
|
LocalExecutor
|
|
152
160
|
} from "./chunk-6FH4AXVY.js";
|
|
153
161
|
import {
|
|
154
|
-
EDGE_CONTAINER_NAME,
|
|
155
|
-
edgeCrashReason,
|
|
156
162
|
invalidateEdgeContainer,
|
|
157
163
|
sanitizeEdgeVhosts
|
|
158
|
-
} from "./chunk-
|
|
164
|
+
} from "./chunk-WE7BXW6L.js";
|
|
159
165
|
import {
|
|
160
166
|
EDGE_CONTAINER_MOUNTS,
|
|
161
167
|
EDGE_HOST_STATE_DIR,
|
|
162
168
|
OPENRESTY_DEFAULT_PATHS
|
|
163
|
-
} from "./chunk-
|
|
169
|
+
} from "./chunk-PZRRL5PC.js";
|
|
170
|
+
import {
|
|
171
|
+
envOps
|
|
172
|
+
} from "./chunk-EZPPTWJQ.js";
|
|
173
|
+
import {
|
|
174
|
+
ENVIRONMENT_PROBE_SCRIPT,
|
|
175
|
+
ENVIRONMENT_PROBE_TIMEOUT_MS,
|
|
176
|
+
cacheEnvironment,
|
|
177
|
+
parseEnvironmentProbe,
|
|
178
|
+
profileIsFresh
|
|
179
|
+
} from "./chunk-WFRXYWK2.js";
|
|
164
180
|
import {
|
|
165
181
|
DEFAULT_IMAGE_REGISTRY,
|
|
182
|
+
FAMILY_BY_DISTRO,
|
|
183
|
+
HOST_CHANNEL_BLOCKED,
|
|
184
|
+
HOST_CHANNEL_RECHECK,
|
|
185
|
+
HOST_CHANNEL_UNAFFECTED,
|
|
186
|
+
explainHostChannelCause,
|
|
166
187
|
isAwsS3Endpoint,
|
|
167
188
|
safeErrorMessage,
|
|
168
|
-
withTimeout
|
|
169
|
-
|
|
189
|
+
withTimeout,
|
|
190
|
+
wrapText
|
|
191
|
+
} from "./chunk-LEYSL46B.js";
|
|
192
|
+
|
|
193
|
+
// src/lib/ports.ts
|
|
194
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
195
|
+
import { join } from "path";
|
|
196
|
+
|
|
197
|
+
// ../../packages/core/src/ports.ts
|
|
198
|
+
import { createServer } from "net";
|
|
199
|
+
var DEFAULT_API_PORT = 4e3;
|
|
200
|
+
var DEFAULT_DASHBOARD_PORT = 3001;
|
|
201
|
+
var LOOPBACK = "127.0.0.1";
|
|
202
|
+
var SCAN_SPAN = 20;
|
|
203
|
+
var MAX_PORT = 65535;
|
|
204
|
+
function isPortFree(port, host = LOOPBACK) {
|
|
205
|
+
return new Promise((resolve3) => {
|
|
206
|
+
const srv = createServer();
|
|
207
|
+
srv.once("error", () => resolve3(false));
|
|
208
|
+
srv.listen(port, host, () => srv.close(() => resolve3(true)));
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
async function waitPortFree(port, opts = {}) {
|
|
212
|
+
const timeoutMs = opts.timeoutMs ?? 6e3;
|
|
213
|
+
const intervalMs = opts.intervalMs ?? 250;
|
|
214
|
+
const host = opts.host ?? LOOPBACK;
|
|
215
|
+
const deadline = Date.now() + timeoutMs;
|
|
216
|
+
for (; ; ) {
|
|
217
|
+
if (await isPortFree(port, host)) return true;
|
|
218
|
+
if (Date.now() >= deadline) return false;
|
|
219
|
+
await new Promise((r2) => setTimeout(r2, intervalMs));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function getFreePort(host = LOOPBACK) {
|
|
223
|
+
return new Promise((resolve3, reject) => {
|
|
224
|
+
const srv = createServer();
|
|
225
|
+
srv.once("error", reject);
|
|
226
|
+
srv.listen(0, host, () => {
|
|
227
|
+
const addr = srv.address();
|
|
228
|
+
const port = addr && typeof addr === "object" ? addr.port : 0;
|
|
229
|
+
srv.close(() => port ? resolve3(port) : reject(new Error("no free port")));
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
async function findPortNear(from, opts = {}) {
|
|
234
|
+
const host = opts.host ?? LOOPBACK;
|
|
235
|
+
const span = opts.span ?? SCAN_SPAN;
|
|
236
|
+
const skip = opts.skip ?? (() => false);
|
|
237
|
+
for (let port2 = from; port2 <= Math.min(from + span, MAX_PORT); port2++) {
|
|
238
|
+
if (!skip(port2) && await isPortFree(port2, host)) return port2;
|
|
239
|
+
}
|
|
240
|
+
let port = await getFreePort(host);
|
|
241
|
+
for (let tries = 0; tries < 10 && skip(port); tries++) port = await getFreePort(host);
|
|
242
|
+
return port;
|
|
243
|
+
}
|
|
244
|
+
async function resolvePortPair(prefs = {}) {
|
|
245
|
+
const host = prefs.bindAddr?.trim() || LOOPBACK;
|
|
246
|
+
const ours = new Set(prefs.reclaimable ?? []);
|
|
247
|
+
const prefFor = (kind) => prefs[kind] ?? prefs.previous?.[kind] ?? prefs.stored?.[kind] ?? prefs.defaults?.[kind];
|
|
248
|
+
const remembered = (kind, pref) => prefs[kind] === void 0 && (prefs.previous?.[kind] === pref || prefs.stored?.[kind] === pref);
|
|
249
|
+
const availableForPref = async (port) => ours.has(port) || await isPortFree(port, host);
|
|
250
|
+
const apiPref = prefFor("api");
|
|
251
|
+
const dashPref = prefFor("dashboard");
|
|
252
|
+
let api;
|
|
253
|
+
if (apiPref === void 0) api = await getFreePort(host);
|
|
254
|
+
else if (await availableForPref(apiPref)) api = apiPref;
|
|
255
|
+
else if (remembered("api", apiPref) && await waitPortFree(apiPref, { host })) api = apiPref;
|
|
256
|
+
else {
|
|
257
|
+
api = await findPortNear(apiPref + 1, {
|
|
258
|
+
host,
|
|
259
|
+
skip: (p2) => ours.has(p2) || p2 === dashPref
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
const skipForDash = (p2) => ours.has(p2) || p2 === api;
|
|
263
|
+
let dashboard;
|
|
264
|
+
if (dashPref === void 0) {
|
|
265
|
+
dashboard = await getFreePort(host);
|
|
266
|
+
for (let tries = 0; tries < 10 && dashboard === api; tries++) {
|
|
267
|
+
dashboard = await getFreePort(host);
|
|
268
|
+
}
|
|
269
|
+
} else if (dashPref !== api && await availableForPref(dashPref)) {
|
|
270
|
+
dashboard = dashPref;
|
|
271
|
+
} else if (dashPref !== api && remembered("dashboard", dashPref) && await waitPortFree(dashPref, { host })) {
|
|
272
|
+
dashboard = dashPref;
|
|
273
|
+
} else {
|
|
274
|
+
dashboard = await findPortNear(dashPref + 1, { host, skip: skipForDash });
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
api,
|
|
278
|
+
dashboard,
|
|
279
|
+
switched: {
|
|
280
|
+
api: apiPref !== void 0 && api !== apiPref,
|
|
281
|
+
dashboard: dashPref !== void 0 && dashboard !== dashPref
|
|
282
|
+
},
|
|
283
|
+
preferred: { api: apiPref ?? api, dashboard: dashPref ?? dashboard }
|
|
284
|
+
};
|
|
285
|
+
}
|
|
170
286
|
|
|
171
|
-
// src/lib/
|
|
172
|
-
|
|
173
|
-
|
|
287
|
+
// src/lib/ports.ts
|
|
288
|
+
var PORTS_FILE = join(OS_DIR, "ports.json");
|
|
289
|
+
var INSTANCE_FILE = join(OS_DIR, "instance.json");
|
|
290
|
+
function saveInstanceUrl(publicUrl) {
|
|
291
|
+
try {
|
|
292
|
+
if (!existsSync(OS_DIR)) mkdirSync(OS_DIR, { recursive: true, mode: 448 });
|
|
293
|
+
writeFileSync(INSTANCE_FILE, JSON.stringify({ publicUrl: publicUrl ?? null }));
|
|
294
|
+
} catch {
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function readInstanceUrl() {
|
|
298
|
+
try {
|
|
299
|
+
return JSON.parse(readFileSync(INSTANCE_FILE, "utf8")).publicUrl ?? null;
|
|
300
|
+
} catch {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function readStoredPorts() {
|
|
305
|
+
try {
|
|
306
|
+
return JSON.parse(readFileSync(PORTS_FILE, "utf-8"));
|
|
307
|
+
} catch {
|
|
308
|
+
return {};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function storedApiPort() {
|
|
312
|
+
return readStoredPorts().api ?? DEFAULT_API_PORT;
|
|
313
|
+
}
|
|
314
|
+
function storedDashboardPort() {
|
|
315
|
+
return readStoredPorts().dashboard ?? DEFAULT_DASHBOARD_PORT;
|
|
316
|
+
}
|
|
317
|
+
function saveStoredPorts(api, dashboard) {
|
|
318
|
+
try {
|
|
319
|
+
if (!existsSync(OS_DIR)) mkdirSync(OS_DIR, { recursive: true, mode: 448 });
|
|
320
|
+
writeFileSync(PORTS_FILE, JSON.stringify({ api, dashboard }));
|
|
321
|
+
} catch {
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function portMoveNotice(resolved, trustedOriginUrls = []) {
|
|
325
|
+
if (!resolved.switched.api && !resolved.switched.dashboard) return [];
|
|
326
|
+
const lines = [
|
|
327
|
+
`A preferred port was busy \u2014 using API ${resolved.api}, dashboard ${resolved.dashboard}.`
|
|
328
|
+
];
|
|
329
|
+
const stale = [...new Set(stalePortOrigins(resolved.preferred.dashboard, trustedOriginUrls))];
|
|
330
|
+
if (stale.length) {
|
|
331
|
+
lines.push(
|
|
332
|
+
`Heads up: ${stale.join(", ")} still names port ${resolved.preferred.dashboard}, so logins`,
|
|
333
|
+
`from that origin will be rejected. Point it at :${resolved.dashboard} (or free port`,
|
|
334
|
+
`${resolved.preferred.dashboard} and re-run to move back).`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
return lines;
|
|
338
|
+
}
|
|
339
|
+
function stalePortOrigins(movedFrom, urls) {
|
|
340
|
+
const stale = [];
|
|
341
|
+
for (const raw of urls) {
|
|
342
|
+
for (const candidate of (raw ?? "").split(",")) {
|
|
343
|
+
const value = candidate.trim();
|
|
344
|
+
if (!value) continue;
|
|
345
|
+
try {
|
|
346
|
+
if (new URL(value).port === String(movedFrom)) stale.push(value);
|
|
347
|
+
} catch {
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return stale;
|
|
352
|
+
}
|
|
353
|
+
async function resolvePorts(prefs, opts = {}) {
|
|
354
|
+
const resolved = await resolvePortPair({
|
|
355
|
+
...prefs,
|
|
356
|
+
stored: readStoredPorts(),
|
|
357
|
+
defaults: { api: DEFAULT_API_PORT, dashboard: DEFAULT_DASHBOARD_PORT }
|
|
358
|
+
});
|
|
359
|
+
if (opts.persist !== false) saveStoredPorts(resolved.api, resolved.dashboard);
|
|
360
|
+
return resolved;
|
|
361
|
+
}
|
|
174
362
|
|
|
175
363
|
// ../../packages/adapters/src/runtime/image-transfer.ts
|
|
176
364
|
import { Transform } from "stream";
|
|
@@ -204,6 +392,11 @@ var MAIL_DB_PGDATA = `${MAIL_DB_CONTAINER_DATA_DIR}/pgdata`;
|
|
|
204
392
|
var MAIL_HOST_PATHS = {
|
|
205
393
|
saslPasswd: `${MAIL_HOST_STATE_DIR}/config/postfix/sasl_passwd`,
|
|
206
394
|
senderRelayhost: `${MAIL_HOST_STATE_DIR}/config/postfix/sender_relayhost`,
|
|
395
|
+
// Per-nexthop TLS policy. Only split ("selected"-scope) relaying writes it: the
|
|
396
|
+
// relay hop must be `encrypt` so SASL credentials never cross a plaintext
|
|
397
|
+
// connection, while direct-to-MX delivery for the un-relayed domains stays
|
|
398
|
+
// opportunistic. A GLOBAL smtp_tls_security_level can't express both.
|
|
399
|
+
relayTlsPolicy: `${MAIL_HOST_STATE_DIR}/config/postfix/openship_tls_policy`,
|
|
207
400
|
amavisUserConf: `${MAIL_HOST_STATE_DIR}/config/amavis/50-user`
|
|
208
401
|
};
|
|
209
402
|
|
|
@@ -212,6 +405,63 @@ var MAIL_DB_IMAGE = process.env.OPENSHIP_MAIL_DB_IMAGE?.trim() || "postgres:16-a
|
|
|
212
405
|
var ENGINE_ENV_FILE = `${MAIL_HOST_STATE_DIR}/engine.env`;
|
|
213
406
|
var DB_ENV_FILE = `${MAIL_HOST_STATE_DIR}/db.env`;
|
|
214
407
|
|
|
408
|
+
// ../../packages/adapters/src/system/environment-local.ts
|
|
409
|
+
import { spawnSync } from "child_process";
|
|
410
|
+
var cached = null;
|
|
411
|
+
function unmeasurable(reason) {
|
|
412
|
+
return {
|
|
413
|
+
...parseEnvironmentProbe({ text: "", failed: true, unanswered: true }),
|
|
414
|
+
unsupportedReason: reason
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
function probeLocalHost() {
|
|
418
|
+
if (process.platform === "win32") {
|
|
419
|
+
return unmeasurable(
|
|
420
|
+
"Openship cannot provision this machine: it runs Windows, and Openship's host provisioning is POSIX-only. Deploying to a remote Linux server from here works."
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
const result = spawnSync("sh", ["-c", ENVIRONMENT_PROBE_SCRIPT], {
|
|
424
|
+
encoding: "utf8",
|
|
425
|
+
timeout: ENVIRONMENT_PROBE_TIMEOUT_MS,
|
|
426
|
+
// `sh -c` and not the login shell: `wrapLocalBuildCommand`'s `-lc` exists so user
|
|
427
|
+
// builds see a normal profile, but a host probe wants the plain POSIX behaviour the
|
|
428
|
+
// remote path gets, without a dotfile's `echo` in the middle of the output.
|
|
429
|
+
windowsHide: true
|
|
430
|
+
});
|
|
431
|
+
if (result.error) {
|
|
432
|
+
return unmeasurable(
|
|
433
|
+
`Openship could not run a shell on this machine to detect it: ${safeErrorMessage(result.error)}`
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
if (result.signal || typeof result.status !== "number") {
|
|
437
|
+
return unmeasurable(
|
|
438
|
+
`Openship could not measure this machine: the detection probe was killed${result.signal ? ` by ${result.signal}` : ""} before it finished.`
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
const stdout = result.stdout ?? "";
|
|
442
|
+
return parseEnvironmentProbe({
|
|
443
|
+
// The script always exits 0, so a non-zero status means something ran instead of it
|
|
444
|
+
// — the same signal the SSH path uses, and stderr is what said so.
|
|
445
|
+
text: stdout.trim() ? stdout : result.stderr ?? "",
|
|
446
|
+
failed: result.status !== 0,
|
|
447
|
+
// Every way of not answering is refused above, so the shell exited on its own and
|
|
448
|
+
// whatever it said is this machine's.
|
|
449
|
+
unanswered: false
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
function resolveLocalEnvironmentSync() {
|
|
453
|
+
if (cached && profileIsFresh(cached.measuredAt)) return cached.profile;
|
|
454
|
+
const profile = probeLocalHost();
|
|
455
|
+
cached = { measuredAt: Date.now(), profile };
|
|
456
|
+
cacheEnvironment(createExecutor(), profile);
|
|
457
|
+
return profile;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ../../packages/adapters/src/system/modules/types.ts
|
|
461
|
+
var TARGETABLE_FAMILIES = new Set(
|
|
462
|
+
Object.values(FAMILY_BY_DISTRO)
|
|
463
|
+
);
|
|
464
|
+
|
|
215
465
|
// ../../packages/adapters/src/system/modules/verify.ts
|
|
216
466
|
import { createHash, createPublicKey, verify as cryptoVerify } from "crypto";
|
|
217
467
|
|
|
@@ -219,10 +469,6 @@ import { createHash, createPublicKey, verify as cryptoVerify } from "crypto";
|
|
|
219
469
|
var CATALOG_BASE_URL = process.env.OPENSHIP_MODULE_CATALOG_BASE_URL?.trim() || "https://raw.githubusercontent.com/openship/native-modules";
|
|
220
470
|
var CATALOG_REF = process.env.OPENSHIP_MODULE_CATALOG_REF?.trim() || "main";
|
|
221
471
|
|
|
222
|
-
// ../../packages/adapters/src/system/reachability.ts
|
|
223
|
-
import { connect } from "net";
|
|
224
|
-
import { request } from "http";
|
|
225
|
-
|
|
226
472
|
// ../../packages/adapters/src/backup/registry.ts
|
|
227
473
|
var executorFactories = /* @__PURE__ */ new Map();
|
|
228
474
|
function registerExecutor(runtimeName, factory) {
|
|
@@ -747,12 +993,14 @@ var DockerBackupExecutor = class {
|
|
|
747
993
|
try {
|
|
748
994
|
const data = await this.dockerode.getContainer(service.containerId).inspect();
|
|
749
995
|
const mounts = data.Mounts ?? [];
|
|
750
|
-
return mounts.filter((m2) => m2.Type === "volume" || m2.Type === "bind").map(
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
996
|
+
return mounts.filter((m2) => m2.Type === "volume" || m2.Type === "bind").map(
|
|
997
|
+
(m2, i2) => ({
|
|
998
|
+
id: m2.Name ?? m2.Source ?? `mount-${i2}`,
|
|
999
|
+
target: m2.Destination ?? "",
|
|
1000
|
+
source: m2.Name ?? m2.Source ?? "",
|
|
1001
|
+
type: m2.Type ?? "volume"
|
|
1002
|
+
})
|
|
1003
|
+
);
|
|
756
1004
|
} catch {
|
|
757
1005
|
}
|
|
758
1006
|
}
|
|
@@ -784,7 +1032,9 @@ var DockerBackupExecutor = class {
|
|
|
784
1032
|
Env: opts?.env ? Object.entries(opts.env).map(([k2, v2]) => `${k2}=${v2}`) : void 0
|
|
785
1033
|
});
|
|
786
1034
|
const stream = await exec.start({ stdin: false });
|
|
787
|
-
return this.attachDemuxed(this.dockerode, exec.id, stream, opts
|
|
1035
|
+
return this.attachDemuxed(this.dockerode, exec.id, stream, opts, {
|
|
1036
|
+
label: `exec in service ${service.name}`
|
|
1037
|
+
});
|
|
788
1038
|
}
|
|
789
1039
|
async streamPath(service, sourceId, opts) {
|
|
790
1040
|
const sources = await this.listSources(service);
|
|
@@ -796,10 +1046,7 @@ var DockerBackupExecutor = class {
|
|
|
796
1046
|
throw new Error(`Backup source "${sourceId}" is tmpfs \u2014 not backupable`);
|
|
797
1047
|
}
|
|
798
1048
|
const compression = opts?.compression ?? "zstd";
|
|
799
|
-
const excludeArgs = (opts?.exclude ?? []).flatMap((p2) => [
|
|
800
|
-
"--exclude",
|
|
801
|
-
shellEscape4(p2)
|
|
802
|
-
]);
|
|
1049
|
+
const excludeArgs = (opts?.exclude ?? []).flatMap((p2) => ["--exclude", shellEscape4(p2)]);
|
|
803
1050
|
const tarFlags = compressionFlag(compression);
|
|
804
1051
|
const tarCmd = compression === "zstd" ? `tar -c${tarFlags} -C /mnt ${excludeArgs.join(" ")} . | zstd -c -3` : `tar -c${tarFlags} -C /mnt ${excludeArgs.join(" ")} .`;
|
|
805
1052
|
const helperImage = compression === "zstd" ? "alpine:3" : HELPER_IMAGE;
|
|
@@ -824,7 +1071,11 @@ var DockerBackupExecutor = class {
|
|
|
824
1071
|
return this.handOffHelper(
|
|
825
1072
|
{
|
|
826
1073
|
Image: helperImage,
|
|
827
|
-
Cmd: [
|
|
1074
|
+
Cmd: [
|
|
1075
|
+
"sh",
|
|
1076
|
+
"-c",
|
|
1077
|
+
compression === "zstd" ? `apk add --no-cache zstd >/dev/null 2>&1; ${tarCmd}` : tarCmd
|
|
1078
|
+
],
|
|
828
1079
|
HostConfig: hostConfig,
|
|
829
1080
|
AttachStdout: true,
|
|
830
1081
|
AttachStderr: true,
|
|
@@ -1170,7 +1421,7 @@ var DockerBackupExecutor = class {
|
|
|
1170
1421
|
} catch {
|
|
1171
1422
|
}
|
|
1172
1423
|
}, opts.timeoutMs) : null;
|
|
1173
|
-
return new Promise((
|
|
1424
|
+
return new Promise((resolve3, reject) => {
|
|
1174
1425
|
body.on("error", (err) => {
|
|
1175
1426
|
if (timer) clearTimeout(timer);
|
|
1176
1427
|
try {
|
|
@@ -1182,7 +1433,7 @@ var DockerBackupExecutor = class {
|
|
|
1182
1433
|
stream.on("end", async () => {
|
|
1183
1434
|
if (timer) clearTimeout(timer);
|
|
1184
1435
|
try {
|
|
1185
|
-
|
|
1436
|
+
resolve3({
|
|
1186
1437
|
code: await resolveExecExitCode(exec, exec.id),
|
|
1187
1438
|
stderr: Buffer.concat(stderrChunks).toString("utf8").slice(0, 16 * 1024)
|
|
1188
1439
|
});
|
|
@@ -1230,7 +1481,10 @@ var DockerBackupExecutor = class {
|
|
|
1230
1481
|
}
|
|
1231
1482
|
/** dockerode `exec.start` returns a multiplexed stream — stdout +
|
|
1232
1483
|
* stderr interleaved with frame headers. demux into clean streams. */
|
|
1233
|
-
attachDemuxed(docker, execId, stream,
|
|
1484
|
+
attachDemuxed(docker, execId, stream, opts, meta) {
|
|
1485
|
+
const label = meta?.label ?? "exec";
|
|
1486
|
+
const idleMs = opts?.idleTimeoutMs ?? DEFAULT_HELPER_IDLE_MS;
|
|
1487
|
+
const timeoutMs = opts?.timeoutMs ?? DEFAULT_HELPER_TIMEOUT_MS;
|
|
1234
1488
|
const stdout = new PassThrough();
|
|
1235
1489
|
const stderrChunks = [];
|
|
1236
1490
|
const stderrSink = new PassThrough();
|
|
@@ -1238,34 +1492,54 @@ var DockerBackupExecutor = class {
|
|
|
1238
1492
|
if (stderrChunks.length < 16) stderrChunks.push(chunk);
|
|
1239
1493
|
});
|
|
1240
1494
|
docker.modem.demuxStream(stream, stdout, stderrSink);
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1495
|
+
let sinksEnded = false;
|
|
1496
|
+
const endSinks = () => {
|
|
1497
|
+
if (sinksEnded) return;
|
|
1498
|
+
sinksEnded = true;
|
|
1499
|
+
stdout.end();
|
|
1500
|
+
stderrSink.end();
|
|
1501
|
+
};
|
|
1502
|
+
stream.on("end", endSinks);
|
|
1503
|
+
stream.on("close", endSinks);
|
|
1504
|
+
const watchdog = createIdleWatchdog(
|
|
1505
|
+
idleMs,
|
|
1506
|
+
`${label} produced no data for ${Math.round(idleMs / 1e3)}s and was abandoned.`
|
|
1507
|
+
);
|
|
1508
|
+
stream.on("data", () => watchdog.touch());
|
|
1509
|
+
const awaitExit = (async () => {
|
|
1510
|
+
const onEnd = new Promise((resolve3, reject) => {
|
|
1511
|
+
stream.on("end", async () => {
|
|
1512
|
+
try {
|
|
1513
|
+
resolve3({
|
|
1514
|
+
code: await resolveExecExitCode(docker.getExec(execId), execId),
|
|
1515
|
+
stderr: Buffer.concat(stderrChunks).toString("utf8").slice(0, 16 * 1024)
|
|
1516
|
+
});
|
|
1517
|
+
} catch (err) {
|
|
1518
|
+
reject(err);
|
|
1519
|
+
}
|
|
1520
|
+
});
|
|
1521
|
+
stream.on("error", (err) => reject(err));
|
|
1522
|
+
});
|
|
1523
|
+
try {
|
|
1524
|
+
return await withTimeout(
|
|
1525
|
+
Promise.race([onEnd, watchdog.promise]),
|
|
1526
|
+
timeoutMs,
|
|
1527
|
+
`${label} exceeded its ${Math.round(timeoutMs / 1e3)}s ceiling and was abandoned.`
|
|
1528
|
+
);
|
|
1529
|
+
} catch (err) {
|
|
1530
|
+
stdout.destroy(err);
|
|
1531
|
+
throw err;
|
|
1532
|
+
} finally {
|
|
1533
|
+
watchdog.dispose();
|
|
1534
|
+
}
|
|
1535
|
+
})();
|
|
1536
|
+
return { stdout, awaitExit };
|
|
1537
|
+
}
|
|
1538
|
+
demuxContainerStream(container, stream, opts) {
|
|
1539
|
+
const stdout = new PassThrough();
|
|
1540
|
+
const stderrChunks = [];
|
|
1541
|
+
const stderrSink = new PassThrough();
|
|
1542
|
+
stderrSink.on("data", (chunk) => {
|
|
1269
1543
|
if (stderrChunks.length < 16) stderrChunks.push(chunk);
|
|
1270
1544
|
});
|
|
1271
1545
|
container.modem.demuxStream(stream, stdout, stderrSink);
|
|
@@ -1359,7 +1633,7 @@ var CloudBackupExecutor = class {
|
|
|
1359
1633
|
const stdout = new Readable2({ read() {
|
|
1360
1634
|
} });
|
|
1361
1635
|
let stderrBuf = "";
|
|
1362
|
-
const awaitExit = new Promise((
|
|
1636
|
+
const awaitExit = new Promise((resolve3, reject) => {
|
|
1363
1637
|
const timer = opts?.timeoutMs ? setTimeout(() => {
|
|
1364
1638
|
stdout.destroy(new Error(`exec timed out after ${opts.timeoutMs}ms`));
|
|
1365
1639
|
reject(new Error(`exec timed out after ${opts.timeoutMs}ms`));
|
|
@@ -1392,7 +1666,7 @@ var CloudBackupExecutor = class {
|
|
|
1392
1666
|
}
|
|
1393
1667
|
stdout.push(null);
|
|
1394
1668
|
if (timer) clearTimeout(timer);
|
|
1395
|
-
|
|
1669
|
+
resolve3({ code: exitCode, stderr: stderrBuf });
|
|
1396
1670
|
} catch (err) {
|
|
1397
1671
|
if (timer) clearTimeout(timer);
|
|
1398
1672
|
stdout.destroy(err);
|
|
@@ -1501,7 +1775,7 @@ import { pipeline } from "stream/promises";
|
|
|
1501
1775
|
import { createWriteStream, promises as fs } from "fs";
|
|
1502
1776
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
1503
1777
|
import { tmpdir } from "os";
|
|
1504
|
-
import { join } from "path";
|
|
1778
|
+
import { join as join2 } from "path";
|
|
1505
1779
|
var BareBackupExecutor = class {
|
|
1506
1780
|
constructor(runtime) {
|
|
1507
1781
|
this.runtime = runtime;
|
|
@@ -1575,8 +1849,8 @@ var BareBackupExecutor = class {
|
|
|
1575
1849
|
async pipeIntoCommand(service, cmd, body, opts) {
|
|
1576
1850
|
const exec = this.executor();
|
|
1577
1851
|
const id = randomBytes2(6).toString("hex");
|
|
1578
|
-
const localDir =
|
|
1579
|
-
const localFile =
|
|
1852
|
+
const localDir = join2(tmpdir(), `openship-bare-restore-${id}`);
|
|
1853
|
+
const localFile = join2(localDir, "artifact.bin");
|
|
1580
1854
|
const remoteDir = `/tmp/openship-bare-restore-${id}`;
|
|
1581
1855
|
const remoteFile = `${remoteDir}/artifact.bin`;
|
|
1582
1856
|
await fs.mkdir(localDir, { recursive: true });
|
|
@@ -1625,7 +1899,7 @@ registerExecutor("bare", (runtime) => {
|
|
|
1625
1899
|
// ../../packages/adapters/src/backup/destinations/local.ts
|
|
1626
1900
|
import { createHash as createHash4, randomBytes as randomBytes3 } from "crypto";
|
|
1627
1901
|
import { createReadStream, createWriteStream as createWriteStream2, promises as fs2 } from "fs";
|
|
1628
|
-
import { dirname, join as
|
|
1902
|
+
import { dirname, join as join3, normalize, posix, resolve } from "path";
|
|
1629
1903
|
import { pipeline as pipeline2 } from "stream";
|
|
1630
1904
|
import { promisify } from "util";
|
|
1631
1905
|
var pipelineP = promisify(pipeline2);
|
|
@@ -1648,13 +1922,13 @@ var LocalDestinationImpl = class {
|
|
|
1648
1922
|
if (normalized.startsWith("..") || normalized.includes(`${normalize("../")}`)) {
|
|
1649
1923
|
throw new Error(`Invalid key (traversal): ${key}`);
|
|
1650
1924
|
}
|
|
1651
|
-
return
|
|
1925
|
+
return join3(this.root, normalized);
|
|
1652
1926
|
}
|
|
1653
1927
|
async preflight() {
|
|
1654
1928
|
try {
|
|
1655
1929
|
await fs2.mkdir(this.root, { recursive: true });
|
|
1656
1930
|
const probeKey = `.openship-probe-${randomBytes3(6).toString("hex")}`;
|
|
1657
|
-
const probePath =
|
|
1931
|
+
const probePath = join3(this.root, probeKey);
|
|
1658
1932
|
await fs2.writeFile(probePath, "ok");
|
|
1659
1933
|
await fs2.readFile(probePath);
|
|
1660
1934
|
await fs2.unlink(probePath);
|
|
@@ -1728,7 +2002,7 @@ var LocalDestinationImpl = class {
|
|
|
1728
2002
|
for (const dirent of dirents) {
|
|
1729
2003
|
if (entries.length >= limit) return;
|
|
1730
2004
|
const childRel = posix.join(relBase, dirent.name);
|
|
1731
|
-
const childAbs =
|
|
2005
|
+
const childAbs = join3(dir, dirent.name);
|
|
1732
2006
|
if (dirent.isDirectory()) {
|
|
1733
2007
|
await walk(childAbs, childRel);
|
|
1734
2008
|
} else if (dirent.isFile()) {
|
|
@@ -2370,8 +2644,8 @@ var flexibleChecksumsMiddleware = (config, middlewareConfig) => (next, context)
|
|
|
2370
2644
|
if (hasHeaderWithPrefix("x-amz-checksum-", args.request.headers)) {
|
|
2371
2645
|
return next(args);
|
|
2372
2646
|
}
|
|
2373
|
-
const { request
|
|
2374
|
-
const { body: requestBody, headers } =
|
|
2647
|
+
const { request, input } = args;
|
|
2648
|
+
const { body: requestBody, headers } = request;
|
|
2375
2649
|
const { base64Encoder, streamHasher } = config;
|
|
2376
2650
|
const { requestChecksumRequired, requestAlgorithmMember } = middlewareConfig;
|
|
2377
2651
|
const requestChecksumCalculation = await config.requestChecksumCalculation();
|
|
@@ -2442,7 +2716,7 @@ var flexibleChecksumsMiddleware = (config, middlewareConfig) => (next, context)
|
|
|
2442
2716
|
const result = await next({
|
|
2443
2717
|
...args,
|
|
2444
2718
|
request: {
|
|
2445
|
-
...
|
|
2719
|
+
...request,
|
|
2446
2720
|
headers: updatedHeaders,
|
|
2447
2721
|
body: updatedBody
|
|
2448
2722
|
}
|
|
@@ -2638,9 +2912,9 @@ var CONTENT_LENGTH_HEADER = "content-length";
|
|
|
2638
2912
|
var DECODED_CONTENT_LENGTH_HEADER = "x-amz-decoded-content-length";
|
|
2639
2913
|
function checkContentLengthHeader() {
|
|
2640
2914
|
return (next, context) => async (args) => {
|
|
2641
|
-
const { request
|
|
2642
|
-
if (HttpRequest.isInstance(
|
|
2643
|
-
if (!(CONTENT_LENGTH_HEADER in
|
|
2915
|
+
const { request } = args;
|
|
2916
|
+
if (HttpRequest.isInstance(request)) {
|
|
2917
|
+
if (!(CONTENT_LENGTH_HEADER in request.headers) && !(DECODED_CONTENT_LENGTH_HEADER in request.headers)) {
|
|
2644
2918
|
const message = `Are you using a Stream of unknown length as the Body of a PutObject request? Consider using Upload instead from @aws-sdk/lib-storage.`;
|
|
2645
2919
|
if (typeof context?.logger?.warn === "function" && !(context.logger instanceof NoOpLogger)) {
|
|
2646
2920
|
context.logger.warn(message);
|
|
@@ -2956,8 +3230,8 @@ var getS3ExpressPlugin = (options) => ({
|
|
|
2956
3230
|
});
|
|
2957
3231
|
|
|
2958
3232
|
// ../../node_modules/@aws-sdk/middleware-sdk-s3/dist-es/submodules/s3/middleware-s3-express/functions/signS3Express.js
|
|
2959
|
-
var signS3Express = async (s3ExpressIdentity, signingOptions,
|
|
2960
|
-
const signedRequest = await sigV4MultiRegionSigner.signWithCredentials(
|
|
3233
|
+
var signS3Express = async (s3ExpressIdentity, signingOptions, request, sigV4MultiRegionSigner) => {
|
|
3234
|
+
const signedRequest = await sigV4MultiRegionSigner.signWithCredentials(request, s3ExpressIdentity, {});
|
|
2961
3235
|
if (signedRequest.headers["X-Amz-Security-Token"] || signedRequest.headers["x-amz-security-token"]) {
|
|
2962
3236
|
throw new Error("X-Amz-Security-Token must not be set for s3-express requests.");
|
|
2963
3237
|
}
|
|
@@ -2980,15 +3254,15 @@ var s3ExpressHttpSigningMiddleware = (config) => (next, context) => async (args)
|
|
|
2980
3254
|
throw new Error(`No HttpAuthScheme was selected: unable to sign request`);
|
|
2981
3255
|
}
|
|
2982
3256
|
const { httpAuthOption: { signingProperties = {} }, identity, signer } = scheme;
|
|
2983
|
-
let
|
|
3257
|
+
let request;
|
|
2984
3258
|
if (context.s3ExpressIdentity) {
|
|
2985
|
-
|
|
3259
|
+
request = await signS3Express(context.s3ExpressIdentity, signingProperties, args.request, await config.signer());
|
|
2986
3260
|
} else {
|
|
2987
|
-
|
|
3261
|
+
request = await signer.sign(args.request, identity, signingProperties);
|
|
2988
3262
|
}
|
|
2989
3263
|
const output = await next({
|
|
2990
3264
|
...args,
|
|
2991
|
-
request
|
|
3265
|
+
request
|
|
2992
3266
|
}).catch((signer.errorHandler || defaultErrorHandler)(signingProperties));
|
|
2993
3267
|
(signer.successHandler || defaultSuccessHandler)(output.response, signingProperties);
|
|
2994
3268
|
return output;
|
|
@@ -3060,9 +3334,9 @@ var getThrow200ExceptionsPlugin = (config) => ({
|
|
|
3060
3334
|
var validate = (str) => typeof str === "string" && str.indexOf("arn:") === 0 && str.split(":").length >= 6;
|
|
3061
3335
|
|
|
3062
3336
|
// ../../node_modules/@aws-sdk/core/dist-es/submodules/util/util-format-url/format-url.js
|
|
3063
|
-
function formatUrl(
|
|
3064
|
-
const { port, query } =
|
|
3065
|
-
let { protocol, path, hostname } =
|
|
3337
|
+
function formatUrl(request) {
|
|
3338
|
+
const { port, query } = request;
|
|
3339
|
+
let { protocol, path, hostname } = request;
|
|
3066
3340
|
if (protocol && protocol.slice(-1) !== ":") {
|
|
3067
3341
|
protocol += ":";
|
|
3068
3342
|
}
|
|
@@ -3077,14 +3351,14 @@ function formatUrl(request2) {
|
|
|
3077
3351
|
queryString = `?${queryString}`;
|
|
3078
3352
|
}
|
|
3079
3353
|
let auth = "";
|
|
3080
|
-
if (
|
|
3081
|
-
const username =
|
|
3082
|
-
const password =
|
|
3354
|
+
if (request.username != null || request.password != null) {
|
|
3355
|
+
const username = request.username ?? "";
|
|
3356
|
+
const password = request.password ?? "";
|
|
3083
3357
|
auth = `${username}:${password}@`;
|
|
3084
3358
|
}
|
|
3085
3359
|
let fragment = "";
|
|
3086
|
-
if (
|
|
3087
|
-
fragment = `#${
|
|
3360
|
+
if (request.fragment) {
|
|
3361
|
+
fragment = `#${request.fragment}`;
|
|
3088
3362
|
}
|
|
3089
3363
|
return `${protocol}//${auth}${hostname}${path}${queryString}${fragment}`;
|
|
3090
3364
|
}
|
|
@@ -3477,18 +3751,18 @@ var AwsRestXmlProtocol = class extends HttpBindingProtocol {
|
|
|
3477
3751
|
return "aws.protocols#restXml";
|
|
3478
3752
|
}
|
|
3479
3753
|
async serializeRequest(operationSchema, input, context) {
|
|
3480
|
-
const
|
|
3754
|
+
const request = await super.serializeRequest(operationSchema, input, context);
|
|
3481
3755
|
const inputSchema = NormalizedSchema.of(operationSchema.input);
|
|
3482
|
-
if (!
|
|
3756
|
+
if (!request.headers["content-type"]) {
|
|
3483
3757
|
const contentType = this.mixin.resolveRestContentType(this.getDefaultContentType(), inputSchema);
|
|
3484
3758
|
if (contentType) {
|
|
3485
|
-
|
|
3759
|
+
request.headers["content-type"] = contentType;
|
|
3486
3760
|
}
|
|
3487
3761
|
}
|
|
3488
|
-
if (typeof
|
|
3489
|
-
|
|
3762
|
+
if (typeof request.body === "string" && request.headers["content-type"] === this.getDefaultContentType() && !request.body.startsWith("<?xml ") && !this.hasUnstructuredPayloadBinding(inputSchema)) {
|
|
3763
|
+
request.body = '<?xml version="1.0" encoding="UTF-8"?>' + request.body;
|
|
3490
3764
|
}
|
|
3491
|
-
return
|
|
3765
|
+
return request;
|
|
3492
3766
|
}
|
|
3493
3767
|
async deserializeResponse(operationSchema, context, response) {
|
|
3494
3768
|
return super.deserializeResponse(operationSchema, context, response);
|
|
@@ -3541,7 +3815,7 @@ var AwsRestXmlProtocol = class extends HttpBindingProtocol {
|
|
|
3541
3815
|
// ../../node_modules/@aws-sdk/middleware-sdk-s3/dist-es/submodules/s3/protocol/S3RestXmlProtocol.js
|
|
3542
3816
|
var S3RestXmlProtocol = class extends AwsRestXmlProtocol {
|
|
3543
3817
|
async serializeRequest(operationSchema, input, context) {
|
|
3544
|
-
const
|
|
3818
|
+
const request = await super.serializeRequest(operationSchema, input, context);
|
|
3545
3819
|
const ns = NormalizedSchema.of(operationSchema.input);
|
|
3546
3820
|
const staticStructureSchema = ns.getSchema();
|
|
3547
3821
|
let bucketMemberIndex = 0;
|
|
@@ -3559,7 +3833,7 @@ var S3RestXmlProtocol = class extends AwsRestXmlProtocol {
|
|
|
3559
3833
|
}
|
|
3560
3834
|
}
|
|
3561
3835
|
}
|
|
3562
|
-
return
|
|
3836
|
+
return request;
|
|
3563
3837
|
}
|
|
3564
3838
|
};
|
|
3565
3839
|
|
|
@@ -3575,12 +3849,12 @@ var NODE_USE_ARN_REGION_CONFIG_OPTIONS = {
|
|
|
3575
3849
|
// ../../node_modules/@aws-sdk/middleware-sdk-s3/dist-es/submodules/s3/middleware-expect-continue/middleware-expect-continue.js
|
|
3576
3850
|
function addExpectContinueMiddleware(options) {
|
|
3577
3851
|
return (next) => async (args) => {
|
|
3578
|
-
const { request
|
|
3579
|
-
if (options.expectContinueHeader !== false && HttpRequest.isInstance(
|
|
3852
|
+
const { request } = args;
|
|
3853
|
+
if (options.expectContinueHeader !== false && HttpRequest.isInstance(request) && request.body && options.runtime === "node" && options.requestHandler?.constructor?.name !== "FetchHttpHandler") {
|
|
3580
3854
|
let sendHeader = true;
|
|
3581
3855
|
if (typeof options.expectContinueHeader === "number") {
|
|
3582
3856
|
try {
|
|
3583
|
-
const bodyLength = Number(
|
|
3857
|
+
const bodyLength = Number(request.headers?.["content-length"]) ?? options.bodyLengthChecker?.(request.body) ?? Infinity;
|
|
3584
3858
|
sendHeader = bodyLength >= options.expectContinueHeader;
|
|
3585
3859
|
} catch (e2) {
|
|
3586
3860
|
}
|
|
@@ -3588,12 +3862,12 @@ function addExpectContinueMiddleware(options) {
|
|
|
3588
3862
|
sendHeader = !!options.expectContinueHeader;
|
|
3589
3863
|
}
|
|
3590
3864
|
if (sendHeader) {
|
|
3591
|
-
|
|
3865
|
+
request.headers.Expect = "100-continue";
|
|
3592
3866
|
}
|
|
3593
3867
|
}
|
|
3594
3868
|
return next({
|
|
3595
3869
|
...args,
|
|
3596
|
-
request
|
|
3870
|
+
request
|
|
3597
3871
|
});
|
|
3598
3872
|
};
|
|
3599
3873
|
}
|
|
@@ -11398,7 +11672,7 @@ var defaultProvider = (init = {}) => memoizeChain([
|
|
|
11398
11672
|
},
|
|
11399
11673
|
async (awsIdentityProperties) => {
|
|
11400
11674
|
init.logger?.debug("@aws-sdk/credential-provider-node - defaultProvider::fromIni");
|
|
11401
|
-
const { fromIni } = await import("./dist-es-
|
|
11675
|
+
const { fromIni } = await import("./dist-es-ZP5QDSJ3.js");
|
|
11402
11676
|
return fromIni(init)(awsIdentityProperties);
|
|
11403
11677
|
},
|
|
11404
11678
|
async (awsIdentityProperties) => {
|
|
@@ -11450,14 +11724,14 @@ var readableStreamHasher = (hashCtor, readableStream) => {
|
|
|
11450
11724
|
const hash = new hashCtor();
|
|
11451
11725
|
const hashCalculator = new HashCalculator(hash);
|
|
11452
11726
|
readableStream.pipe(hashCalculator);
|
|
11453
|
-
return new Promise((
|
|
11727
|
+
return new Promise((resolve3, reject) => {
|
|
11454
11728
|
readableStream.on("error", (err) => {
|
|
11455
11729
|
hashCalculator.end();
|
|
11456
11730
|
reject(err);
|
|
11457
11731
|
});
|
|
11458
11732
|
hashCalculator.on("error", reject);
|
|
11459
11733
|
hashCalculator.on("finish", () => {
|
|
11460
|
-
hash.digest().then(
|
|
11734
|
+
hash.digest().then(resolve3).catch(reject);
|
|
11461
11735
|
});
|
|
11462
11736
|
});
|
|
11463
11737
|
};
|
|
@@ -11890,8 +12164,8 @@ var BYTE_LENGTH_SOURCE;
|
|
|
11890
12164
|
BYTE_LENGTH_SOURCE2["START_END_DIFF"] = "the numeric difference between Body.start and Body.end";
|
|
11891
12165
|
BYTE_LENGTH_SOURCE2["LSTAT"] = "the size of the file given by Body.path on disk as reported by lstatSync";
|
|
11892
12166
|
})(BYTE_LENGTH_SOURCE || (BYTE_LENGTH_SOURCE = {}));
|
|
11893
|
-
var byteLengthSource = (input,
|
|
11894
|
-
if (
|
|
12167
|
+
var byteLengthSource = (input, override) => {
|
|
12168
|
+
if (override != null) {
|
|
11895
12169
|
return BYTE_LENGTH_SOURCE.CONTENT_LENGTH;
|
|
11896
12170
|
}
|
|
11897
12171
|
if (input == null) {
|
|
@@ -12187,8 +12461,8 @@ var Upload = class _Upload extends EventEmitter {
|
|
|
12187
12461
|
const requestHandler = this.client.config.requestHandler;
|
|
12188
12462
|
const eventEmitter = requestHandler instanceof EventEmitter ? requestHandler : null;
|
|
12189
12463
|
let lastSeenBytes = 0;
|
|
12190
|
-
const uploadEventListener = (event,
|
|
12191
|
-
const requestPartSize = Number(
|
|
12464
|
+
const uploadEventListener = (event, request) => {
|
|
12465
|
+
const requestPartSize = Number(request.query["partNumber"]) || -1;
|
|
12192
12466
|
if (requestPartSize !== dataPart.partNumber) {
|
|
12193
12467
|
return;
|
|
12194
12468
|
}
|
|
@@ -12313,7 +12587,7 @@ to input.params.ContentLength in bytes.
|
|
|
12313
12587
|
}
|
|
12314
12588
|
}
|
|
12315
12589
|
async __abortTimeout(abortSignal) {
|
|
12316
|
-
return new Promise((
|
|
12590
|
+
return new Promise((resolve3, reject) => {
|
|
12317
12591
|
abortSignal.onabort = () => {
|
|
12318
12592
|
const abortError = new Error("Upload aborted.");
|
|
12319
12593
|
abortError.name = "AbortError";
|
|
@@ -12426,13 +12700,13 @@ var getSignedUrl = async (client, command, options = {}) => {
|
|
|
12426
12700
|
s3Presigner = new S3RequestPresigner(client.config);
|
|
12427
12701
|
}
|
|
12428
12702
|
const presignInterceptMiddleware = (next, context) => async (args) => {
|
|
12429
|
-
const { request
|
|
12430
|
-
if (!HttpRequest.isInstance(
|
|
12703
|
+
const { request } = args;
|
|
12704
|
+
if (!HttpRequest.isInstance(request)) {
|
|
12431
12705
|
throw new Error("Request to be presigned is not an valid HTTP request.");
|
|
12432
12706
|
}
|
|
12433
|
-
delete
|
|
12434
|
-
delete
|
|
12435
|
-
delete
|
|
12707
|
+
delete request.headers["amz-sdk-invocation-id"];
|
|
12708
|
+
delete request.headers["amz-sdk-request"];
|
|
12709
|
+
delete request.headers["x-amz-user-agent"];
|
|
12436
12710
|
let presigned2;
|
|
12437
12711
|
const presignerOptions = {
|
|
12438
12712
|
...options,
|
|
@@ -12440,9 +12714,9 @@ var getSignedUrl = async (client, command, options = {}) => {
|
|
|
12440
12714
|
signingService: options.signingService ?? context["signing_service"]
|
|
12441
12715
|
};
|
|
12442
12716
|
if (context.s3ExpressIdentity) {
|
|
12443
|
-
presigned2 = await s3Presigner.presignWithCredentials(
|
|
12717
|
+
presigned2 = await s3Presigner.presignWithCredentials(request, context.s3ExpressIdentity, presignerOptions);
|
|
12444
12718
|
} else {
|
|
12445
|
-
presigned2 = await s3Presigner.presign(
|
|
12719
|
+
presigned2 = await s3Presigner.presign(request, presignerOptions);
|
|
12446
12720
|
}
|
|
12447
12721
|
return {
|
|
12448
12722
|
response: {},
|
|
@@ -12680,6 +12954,8 @@ var CAPS3 = /* @__PURE__ */ new Set([
|
|
|
12680
12954
|
"streamingPut",
|
|
12681
12955
|
"streamingGet"
|
|
12682
12956
|
]);
|
|
12957
|
+
var UPLOAD_STALL_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
12958
|
+
var UPLOAD_STALL_PROBE_MS = 30 * 1e3;
|
|
12683
12959
|
var SftpDestinationImpl = class {
|
|
12684
12960
|
kind;
|
|
12685
12961
|
capabilities = CAPS3;
|
|
@@ -12727,7 +13003,7 @@ var SftpDestinationImpl = class {
|
|
|
12727
13003
|
// ── Connection helper ────────────────────────────────────────────────
|
|
12728
13004
|
async withSftp(fn) {
|
|
12729
13005
|
const client = new Client2();
|
|
12730
|
-
return new Promise((
|
|
13006
|
+
return new Promise((resolve3, reject) => {
|
|
12731
13007
|
const cleanup = () => {
|
|
12732
13008
|
try {
|
|
12733
13009
|
client.end();
|
|
@@ -12744,7 +13020,7 @@ var SftpDestinationImpl = class {
|
|
|
12744
13020
|
fn(sftp).then(
|
|
12745
13021
|
(val) => {
|
|
12746
13022
|
cleanup();
|
|
12747
|
-
|
|
13023
|
+
resolve3(val);
|
|
12748
13024
|
},
|
|
12749
13025
|
(e2) => {
|
|
12750
13026
|
cleanup();
|
|
@@ -12764,12 +13040,12 @@ var SftpDestinationImpl = class {
|
|
|
12764
13040
|
let current = dir.startsWith("/") ? "" : ".";
|
|
12765
13041
|
for (const part of parts) {
|
|
12766
13042
|
current = current === "" ? `/${part}` : posix2.join(current, part);
|
|
12767
|
-
await new Promise((
|
|
13043
|
+
await new Promise((resolve3, reject) => {
|
|
12768
13044
|
sftp.mkdir(current, (err) => {
|
|
12769
|
-
if (!err) return
|
|
13045
|
+
if (!err) return resolve3();
|
|
12770
13046
|
sftp.stat(current, (statErr, stats) => {
|
|
12771
13047
|
if (statErr) return reject(err);
|
|
12772
|
-
if (stats.isDirectory()) return
|
|
13048
|
+
if (stats.isDirectory()) return resolve3();
|
|
12773
13049
|
reject(new Error(`${current} exists but is not a directory`));
|
|
12774
13050
|
});
|
|
12775
13051
|
});
|
|
@@ -12783,14 +13059,14 @@ var SftpDestinationImpl = class {
|
|
|
12783
13059
|
await this.withSftp(async (sftp) => {
|
|
12784
13060
|
await this.ensureDir(sftp, this.rootPath);
|
|
12785
13061
|
const probePath = posix2.join(this.rootPath, probeName);
|
|
12786
|
-
await new Promise((
|
|
13062
|
+
await new Promise((resolve3, reject) => {
|
|
12787
13063
|
const ws = sftp.createWriteStream(probePath);
|
|
12788
13064
|
ws.on("error", reject);
|
|
12789
|
-
ws.on("close", () =>
|
|
13065
|
+
ws.on("close", () => resolve3());
|
|
12790
13066
|
ws.end("ok");
|
|
12791
13067
|
});
|
|
12792
13068
|
await new Promise(
|
|
12793
|
-
(
|
|
13069
|
+
(resolve3, reject) => sftp.unlink(probePath, (err) => err ? reject(err) : resolve3())
|
|
12794
13070
|
);
|
|
12795
13071
|
});
|
|
12796
13072
|
return { ok: true };
|
|
@@ -12807,27 +13083,49 @@ var SftpDestinationImpl = class {
|
|
|
12807
13083
|
let bytesWritten = 0;
|
|
12808
13084
|
await this.withSftp(async (sftp) => {
|
|
12809
13085
|
await this.ensureDir(sftp, posix2.dirname(target));
|
|
12810
|
-
await new Promise((
|
|
13086
|
+
await new Promise((resolve3, reject) => {
|
|
12811
13087
|
const ws = sftp.createWriteStream(tmp);
|
|
12812
|
-
|
|
12813
|
-
|
|
13088
|
+
let lastProgressAt = Date.now();
|
|
13089
|
+
let settled = false;
|
|
13090
|
+
let watchdog;
|
|
13091
|
+
const finish = (err) => {
|
|
13092
|
+
if (settled) return;
|
|
13093
|
+
settled = true;
|
|
13094
|
+
if (watchdog) clearInterval(watchdog);
|
|
13095
|
+
if (err) {
|
|
13096
|
+
ws.destroy();
|
|
13097
|
+
body.destroy();
|
|
13098
|
+
reject(err);
|
|
13099
|
+
} else {
|
|
13100
|
+
resolve3();
|
|
13101
|
+
}
|
|
13102
|
+
};
|
|
13103
|
+
watchdog = setInterval(() => {
|
|
13104
|
+
if (settled || Date.now() - lastProgressAt <= UPLOAD_STALL_TIMEOUT_MS) return;
|
|
13105
|
+
finish(
|
|
13106
|
+
new Error(
|
|
13107
|
+
`SFTP upload stalled: no write progress for ${UPLOAD_STALL_TIMEOUT_MS / 1e3}s (wrote ${bytesWritten} bytes so far). Retry; if it recurs, check both ends \u2014 whether the source is still producing bytes, and the destination's SFTP subsystem.`
|
|
13108
|
+
)
|
|
13109
|
+
);
|
|
13110
|
+
}, UPLOAD_STALL_PROBE_MS);
|
|
13111
|
+
watchdog.unref?.();
|
|
13112
|
+
ws.on("error", (err) => finish(err));
|
|
13113
|
+
ws.on("close", () => finish());
|
|
12814
13114
|
body.on("data", (chunk) => {
|
|
12815
13115
|
bytesWritten += chunk.byteLength;
|
|
13116
|
+
lastProgressAt = Date.now();
|
|
12816
13117
|
});
|
|
12817
|
-
body.on("error", (err) =>
|
|
12818
|
-
ws.destroy();
|
|
12819
|
-
reject(err);
|
|
12820
|
-
});
|
|
13118
|
+
body.on("error", (err) => finish(err));
|
|
12821
13119
|
body.pipe(ws);
|
|
12822
13120
|
});
|
|
12823
|
-
await new Promise((
|
|
13121
|
+
await new Promise((resolve3, reject) => {
|
|
12824
13122
|
sftp.rename(tmp, target, (err) => {
|
|
12825
|
-
if (!err) return
|
|
13123
|
+
if (!err) return resolve3();
|
|
12826
13124
|
sftp.unlink(target, () => {
|
|
12827
13125
|
sftp.rename(
|
|
12828
13126
|
tmp,
|
|
12829
13127
|
target,
|
|
12830
|
-
(err2) => err2 ? reject(err2) :
|
|
13128
|
+
(err2) => err2 ? reject(err2) : resolve3()
|
|
12831
13129
|
);
|
|
12832
13130
|
});
|
|
12833
13131
|
});
|
|
@@ -12874,14 +13172,14 @@ var SftpDestinationImpl = class {
|
|
|
12874
13172
|
const target = this.fullPath(key);
|
|
12875
13173
|
try {
|
|
12876
13174
|
return await this.withSftp(
|
|
12877
|
-
(sftp) => new Promise((
|
|
13175
|
+
(sftp) => new Promise((resolve3, reject) => {
|
|
12878
13176
|
sftp.stat(target, (err, stats) => {
|
|
12879
13177
|
if (err) {
|
|
12880
13178
|
const code = err.code;
|
|
12881
|
-
if (code === 2) return
|
|
13179
|
+
if (code === 2) return resolve3(null);
|
|
12882
13180
|
return reject(err);
|
|
12883
13181
|
}
|
|
12884
|
-
|
|
13182
|
+
resolve3({
|
|
12885
13183
|
sizeBytes: stats.size,
|
|
12886
13184
|
uploadedAt: new Date(stats.mtime * 1e3)
|
|
12887
13185
|
});
|
|
@@ -12898,14 +13196,14 @@ var SftpDestinationImpl = class {
|
|
|
12898
13196
|
const entries = [];
|
|
12899
13197
|
await this.withSftp(async (sftp) => {
|
|
12900
13198
|
const walk = async (dir, relBase) => {
|
|
12901
|
-
const dirents = await new Promise((
|
|
13199
|
+
const dirents = await new Promise((resolve3, reject) => {
|
|
12902
13200
|
sftp.readdir(dir, (err, list) => {
|
|
12903
13201
|
if (err) {
|
|
12904
13202
|
const code = err.code;
|
|
12905
|
-
if (code === 2) return
|
|
13203
|
+
if (code === 2) return resolve3([]);
|
|
12906
13204
|
return reject(err);
|
|
12907
13205
|
}
|
|
12908
|
-
|
|
13206
|
+
resolve3(list);
|
|
12909
13207
|
});
|
|
12910
13208
|
});
|
|
12911
13209
|
for (const dirent of dirents) {
|
|
@@ -12931,10 +13229,10 @@ var SftpDestinationImpl = class {
|
|
|
12931
13229
|
const target = this.fullPath(key);
|
|
12932
13230
|
await this.withSftp(
|
|
12933
13231
|
(sftp) => new Promise(
|
|
12934
|
-
(
|
|
12935
|
-
if (!err) return
|
|
13232
|
+
(resolve3, reject) => sftp.unlink(target, (err) => {
|
|
13233
|
+
if (!err) return resolve3();
|
|
12936
13234
|
const code = err.code;
|
|
12937
|
-
if (code === 2) return
|
|
13235
|
+
if (code === 2) return resolve3();
|
|
12938
13236
|
reject(err);
|
|
12939
13237
|
})
|
|
12940
13238
|
)
|
|
@@ -12946,11 +13244,11 @@ var SftpDestinationImpl = class {
|
|
|
12946
13244
|
const failed = [];
|
|
12947
13245
|
await this.withSftp(async (sftp) => {
|
|
12948
13246
|
for (const key of keys) {
|
|
12949
|
-
await new Promise((
|
|
13247
|
+
await new Promise((resolve3) => {
|
|
12950
13248
|
sftp.unlink(this.fullPath(key), (err) => {
|
|
12951
13249
|
if (!err) {
|
|
12952
13250
|
deleted.push(key);
|
|
12953
|
-
return
|
|
13251
|
+
return resolve3();
|
|
12954
13252
|
}
|
|
12955
13253
|
const code = err.code;
|
|
12956
13254
|
if (code === 2) {
|
|
@@ -12958,7 +13256,7 @@ var SftpDestinationImpl = class {
|
|
|
12958
13256
|
} else {
|
|
12959
13257
|
failed.push({ key, error: err.message });
|
|
12960
13258
|
}
|
|
12961
|
-
|
|
13259
|
+
resolve3();
|
|
12962
13260
|
});
|
|
12963
13261
|
});
|
|
12964
13262
|
}
|
|
@@ -12969,210 +13267,29 @@ var SftpDestinationImpl = class {
|
|
|
12969
13267
|
registerDestination("sftp", (row) => new SftpDestinationImpl(row));
|
|
12970
13268
|
registerDestination("openship_server", (row) => new SftpDestinationImpl(row));
|
|
12971
13269
|
|
|
12972
|
-
// src/lib/
|
|
12973
|
-
|
|
12974
|
-
|
|
12975
|
-
import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
12976
|
-
import { homedir as homedir2, userInfo } from "os";
|
|
12977
|
-
import { join as join7 } from "path";
|
|
12978
|
-
|
|
12979
|
-
// src/lib/paths.ts
|
|
12980
|
-
import { homedir } from "os";
|
|
12981
|
-
import { join as join3, resolve as resolve2 } from "path";
|
|
12982
|
-
var DEFAULT_HOME = join3(homedir(), ".openship");
|
|
12983
|
-
var override = process.env.OPENSHIP_HOME?.trim();
|
|
12984
|
-
var OS_DIR = override ? resolve2(override) : DEFAULT_HOME;
|
|
12985
|
-
var IS_ALT_HOME = OS_DIR !== DEFAULT_HOME;
|
|
12986
|
-
var DATA_DIR = join3(OS_DIR, "data");
|
|
12987
|
-
var LOG_DIR = join3(OS_DIR, "logs");
|
|
12988
|
-
var AUTH_SECRET_FILE = join3(OS_DIR, "auth-secret");
|
|
12989
|
-
var INTERNAL_TOKEN_FILE = join3(OS_DIR, "internal-token");
|
|
12990
|
-
|
|
12991
|
-
// src/lib/ports.ts
|
|
12992
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
12993
|
-
import { join as join4 } from "path";
|
|
12994
|
-
|
|
12995
|
-
// ../../packages/core/src/ports.ts
|
|
12996
|
-
import { createServer } from "net";
|
|
12997
|
-
var DEFAULT_API_PORT = 4e3;
|
|
12998
|
-
var DEFAULT_DASHBOARD_PORT = 3001;
|
|
12999
|
-
var LOOPBACK = "127.0.0.1";
|
|
13000
|
-
var SCAN_SPAN = 20;
|
|
13001
|
-
var MAX_PORT = 65535;
|
|
13002
|
-
function isPortFree(port, host = LOOPBACK) {
|
|
13003
|
-
return new Promise((resolve4) => {
|
|
13004
|
-
const srv = createServer();
|
|
13005
|
-
srv.once("error", () => resolve4(false));
|
|
13006
|
-
srv.listen(port, host, () => srv.close(() => resolve4(true)));
|
|
13007
|
-
});
|
|
13270
|
+
// src/lib/this-host.ts
|
|
13271
|
+
function thisHost() {
|
|
13272
|
+
return envOps(resolveLocalEnvironmentSync());
|
|
13008
13273
|
}
|
|
13009
|
-
|
|
13010
|
-
|
|
13011
|
-
const
|
|
13012
|
-
|
|
13013
|
-
const deadline = Date.now() + timeoutMs;
|
|
13014
|
-
for (; ; ) {
|
|
13015
|
-
if (await isPortFree(port, host)) return true;
|
|
13016
|
-
if (Date.now() >= deadline) return false;
|
|
13017
|
-
await new Promise((r2) => setTimeout(r2, intervalMs));
|
|
13018
|
-
}
|
|
13274
|
+
function pasteable(op) {
|
|
13275
|
+
if (!op.supported) return null;
|
|
13276
|
+
const sudo = thisHost().profile.isRoot ? "" : "sudo ";
|
|
13277
|
+
return op.value.map((step) => `${sudo}${step}`).join(" && ");
|
|
13019
13278
|
}
|
|
13020
|
-
function
|
|
13021
|
-
return
|
|
13022
|
-
const srv = createServer();
|
|
13023
|
-
srv.once("error", reject);
|
|
13024
|
-
srv.listen(0, host, () => {
|
|
13025
|
-
const addr = srv.address();
|
|
13026
|
-
const port = addr && typeof addr === "object" ? addr.port : 0;
|
|
13027
|
-
srv.close(() => port ? resolve4(port) : reject(new Error("no free port")));
|
|
13028
|
-
});
|
|
13029
|
-
});
|
|
13279
|
+
function startUnitHint(unit) {
|
|
13280
|
+
return pasteable(thisHost().serviceEnableStart(unit));
|
|
13030
13281
|
}
|
|
13031
|
-
async function findPortNear(from, opts = {}) {
|
|
13032
|
-
const host = opts.host ?? LOOPBACK;
|
|
13033
|
-
const span = opts.span ?? SCAN_SPAN;
|
|
13034
|
-
const skip = opts.skip ?? (() => false);
|
|
13035
|
-
for (let port2 = from; port2 <= Math.min(from + span, MAX_PORT); port2++) {
|
|
13036
|
-
if (!skip(port2) && await isPortFree(port2, host)) return port2;
|
|
13037
|
-
}
|
|
13038
|
-
let port = await getFreePort(host);
|
|
13039
|
-
for (let tries = 0; tries < 10 && skip(port); tries++) port = await getFreePort(host);
|
|
13040
|
-
return port;
|
|
13041
|
-
}
|
|
13042
|
-
async function resolvePortPair(prefs = {}) {
|
|
13043
|
-
const host = prefs.bindAddr?.trim() || LOOPBACK;
|
|
13044
|
-
const ours = new Set(prefs.reclaimable ?? []);
|
|
13045
|
-
const prefFor = (kind) => prefs[kind] ?? prefs.previous?.[kind] ?? prefs.stored?.[kind] ?? prefs.defaults?.[kind];
|
|
13046
|
-
const remembered = (kind, pref) => prefs[kind] === void 0 && (prefs.previous?.[kind] === pref || prefs.stored?.[kind] === pref);
|
|
13047
|
-
const availableForPref = async (port) => ours.has(port) || await isPortFree(port, host);
|
|
13048
|
-
const apiPref = prefFor("api");
|
|
13049
|
-
const dashPref = prefFor("dashboard");
|
|
13050
|
-
let api;
|
|
13051
|
-
if (apiPref === void 0) api = await getFreePort(host);
|
|
13052
|
-
else if (await availableForPref(apiPref)) api = apiPref;
|
|
13053
|
-
else if (remembered("api", apiPref) && await waitPortFree(apiPref, { host })) api = apiPref;
|
|
13054
|
-
else {
|
|
13055
|
-
api = await findPortNear(apiPref + 1, {
|
|
13056
|
-
host,
|
|
13057
|
-
skip: (p2) => ours.has(p2) || p2 === dashPref
|
|
13058
|
-
});
|
|
13059
|
-
}
|
|
13060
|
-
const skipForDash = (p2) => ours.has(p2) || p2 === api;
|
|
13061
|
-
let dashboard;
|
|
13062
|
-
if (dashPref === void 0) {
|
|
13063
|
-
dashboard = await getFreePort(host);
|
|
13064
|
-
for (let tries = 0; tries < 10 && dashboard === api; tries++) {
|
|
13065
|
-
dashboard = await getFreePort(host);
|
|
13066
|
-
}
|
|
13067
|
-
} else if (dashPref !== api && await availableForPref(dashPref)) {
|
|
13068
|
-
dashboard = dashPref;
|
|
13069
|
-
} else if (dashPref !== api && remembered("dashboard", dashPref) && await waitPortFree(dashPref, { host })) {
|
|
13070
|
-
dashboard = dashPref;
|
|
13071
|
-
} else {
|
|
13072
|
-
dashboard = await findPortNear(dashPref + 1, { host, skip: skipForDash });
|
|
13073
|
-
}
|
|
13074
|
-
return {
|
|
13075
|
-
api,
|
|
13076
|
-
dashboard,
|
|
13077
|
-
switched: {
|
|
13078
|
-
api: apiPref !== void 0 && api !== apiPref,
|
|
13079
|
-
dashboard: dashPref !== void 0 && dashboard !== dashPref
|
|
13080
|
-
},
|
|
13081
|
-
preferred: { api: apiPref ?? api, dashboard: dashPref ?? dashboard }
|
|
13082
|
-
};
|
|
13083
|
-
}
|
|
13084
|
-
|
|
13085
|
-
// src/lib/ports.ts
|
|
13086
|
-
var PORTS_FILE = join4(OS_DIR, "ports.json");
|
|
13087
|
-
var INSTANCE_FILE = join4(OS_DIR, "instance.json");
|
|
13088
|
-
function saveInstanceUrl(publicUrl) {
|
|
13089
|
-
try {
|
|
13090
|
-
if (!existsSync(OS_DIR)) mkdirSync(OS_DIR, { recursive: true, mode: 448 });
|
|
13091
|
-
writeFileSync(INSTANCE_FILE, JSON.stringify({ publicUrl: publicUrl ?? null }));
|
|
13092
|
-
} catch {
|
|
13093
|
-
}
|
|
13094
|
-
}
|
|
13095
|
-
function readInstanceUrl() {
|
|
13096
|
-
try {
|
|
13097
|
-
return JSON.parse(readFileSync(INSTANCE_FILE, "utf8")).publicUrl ?? null;
|
|
13098
|
-
} catch {
|
|
13099
|
-
return null;
|
|
13100
|
-
}
|
|
13101
|
-
}
|
|
13102
|
-
function readStoredPorts() {
|
|
13103
|
-
try {
|
|
13104
|
-
return JSON.parse(readFileSync(PORTS_FILE, "utf-8"));
|
|
13105
|
-
} catch {
|
|
13106
|
-
return {};
|
|
13107
|
-
}
|
|
13108
|
-
}
|
|
13109
|
-
function storedApiPort() {
|
|
13110
|
-
return readStoredPorts().api ?? DEFAULT_API_PORT;
|
|
13111
|
-
}
|
|
13112
|
-
function storedDashboardPort() {
|
|
13113
|
-
return readStoredPorts().dashboard ?? DEFAULT_DASHBOARD_PORT;
|
|
13114
|
-
}
|
|
13115
|
-
function saveStoredPorts(api, dashboard) {
|
|
13116
|
-
try {
|
|
13117
|
-
if (!existsSync(OS_DIR)) mkdirSync(OS_DIR, { recursive: true, mode: 448 });
|
|
13118
|
-
writeFileSync(PORTS_FILE, JSON.stringify({ api, dashboard }));
|
|
13119
|
-
} catch {
|
|
13120
|
-
}
|
|
13121
|
-
}
|
|
13122
|
-
function portMoveNotice(resolved, trustedOriginUrls = []) {
|
|
13123
|
-
if (!resolved.switched.api && !resolved.switched.dashboard) return [];
|
|
13124
|
-
const lines = [
|
|
13125
|
-
`A preferred port was busy \u2014 using API ${resolved.api}, dashboard ${resolved.dashboard}.`
|
|
13126
|
-
];
|
|
13127
|
-
const stale = [...new Set(stalePortOrigins(resolved.preferred.dashboard, trustedOriginUrls))];
|
|
13128
|
-
if (stale.length) {
|
|
13129
|
-
lines.push(
|
|
13130
|
-
`Heads up: ${stale.join(", ")} still names port ${resolved.preferred.dashboard}, so logins`,
|
|
13131
|
-
`from that origin will be rejected. Point it at :${resolved.dashboard} (or free port`,
|
|
13132
|
-
`${resolved.preferred.dashboard} and re-run to move back).`
|
|
13133
|
-
);
|
|
13134
|
-
}
|
|
13135
|
-
return lines;
|
|
13136
|
-
}
|
|
13137
|
-
function stalePortOrigins(movedFrom, urls) {
|
|
13138
|
-
const stale = [];
|
|
13139
|
-
for (const raw of urls) {
|
|
13140
|
-
for (const candidate of (raw ?? "").split(",")) {
|
|
13141
|
-
const value = candidate.trim();
|
|
13142
|
-
if (!value) continue;
|
|
13143
|
-
try {
|
|
13144
|
-
if (new URL(value).port === String(movedFrom)) stale.push(value);
|
|
13145
|
-
} catch {
|
|
13146
|
-
}
|
|
13147
|
-
}
|
|
13148
|
-
}
|
|
13149
|
-
return stale;
|
|
13150
|
-
}
|
|
13151
|
-
async function resolvePorts(prefs, opts = {}) {
|
|
13152
|
-
const resolved = await resolvePortPair({
|
|
13153
|
-
...prefs,
|
|
13154
|
-
stored: readStoredPorts(),
|
|
13155
|
-
defaults: { api: DEFAULT_API_PORT, dashboard: DEFAULT_DASHBOARD_PORT }
|
|
13156
|
-
});
|
|
13157
|
-
if (opts.persist !== false) saveStoredPorts(resolved.api, resolved.dashboard);
|
|
13158
|
-
return resolved;
|
|
13159
|
-
}
|
|
13160
|
-
|
|
13161
|
-
// src/lib/source-install.ts
|
|
13162
|
-
import { spawnSync as spawnSync2 } from "child_process";
|
|
13163
|
-
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
13164
|
-
import { join as join6 } from "path";
|
|
13165
13282
|
|
|
13166
13283
|
// src/lib/from-source.ts
|
|
13167
|
-
import { spawn, spawnSync } from "child_process";
|
|
13284
|
+
import { spawn, spawnSync as spawnSync2 } from "child_process";
|
|
13168
13285
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
13169
|
-
import { join as
|
|
13286
|
+
import { join as join4, resolve as resolve2 } from "path";
|
|
13170
13287
|
var DEFAULT_REPO = "https://github.com/oblien/openship.git";
|
|
13171
|
-
var SOURCE_CHECKOUT_DIR =
|
|
13172
|
-
var SOURCE_DIST_DIR =
|
|
13288
|
+
var SOURCE_CHECKOUT_DIR = join4(OS_DIR, "src");
|
|
13289
|
+
var SOURCE_DIST_DIR = join4(OS_DIR, "from-source-dist");
|
|
13173
13290
|
function has(cmd) {
|
|
13174
13291
|
try {
|
|
13175
|
-
return
|
|
13292
|
+
return spawnSync2(cmd, ["--version"], { stdio: "ignore" }).status === 0;
|
|
13176
13293
|
} catch {
|
|
13177
13294
|
return false;
|
|
13178
13295
|
}
|
|
@@ -13194,11 +13311,11 @@ function run(cmd, args, cwd, env) {
|
|
|
13194
13311
|
});
|
|
13195
13312
|
}
|
|
13196
13313
|
function shortSha(cwd) {
|
|
13197
|
-
const r2 =
|
|
13314
|
+
const r2 = spawnSync2("git", ["rev-parse", "--short", "HEAD"], { cwd, encoding: "utf8" });
|
|
13198
13315
|
return r2.status === 0 ? (r2.stdout ?? "").trim() || "unknown" : "unknown";
|
|
13199
13316
|
}
|
|
13200
13317
|
function isMonorepo(dir) {
|
|
13201
|
-
return existsSync2(
|
|
13318
|
+
return existsSync2(join4(dir, "package.json")) && existsSync2(join4(dir, "apps/api/package.json")) && existsSync2(join4(dir, "apps/dashboard/package.json"));
|
|
13202
13319
|
}
|
|
13203
13320
|
async function prepareFromSource(opts) {
|
|
13204
13321
|
if (!has("bun")) {
|
|
@@ -13209,7 +13326,7 @@ async function prepareFromSource(opts) {
|
|
|
13209
13326
|
let sourceDir;
|
|
13210
13327
|
let ref;
|
|
13211
13328
|
if (opts.source) {
|
|
13212
|
-
sourceDir =
|
|
13329
|
+
sourceDir = resolve2(opts.source);
|
|
13213
13330
|
if (!isMonorepo(sourceDir)) {
|
|
13214
13331
|
throw new Error(
|
|
13215
13332
|
`--source ${sourceDir} doesn't look like an Openship checkout (missing package.json / apps/api / apps/dashboard).`
|
|
@@ -13224,7 +13341,7 @@ async function prepareFromSource(opts) {
|
|
|
13224
13341
|
const repoUrl = opts.repo || DEFAULT_REPO;
|
|
13225
13342
|
sourceDir = SOURCE_CHECKOUT_DIR;
|
|
13226
13343
|
mkdirSync2(OS_DIR, { recursive: true });
|
|
13227
|
-
if (!existsSync2(
|
|
13344
|
+
if (!existsSync2(join4(sourceDir, ".git"))) {
|
|
13228
13345
|
console.log(` Cloning ${repoUrl} \u2192 ${sourceDir}`);
|
|
13229
13346
|
await run("git", ["clone", repoUrl, sourceDir], OS_DIR);
|
|
13230
13347
|
}
|
|
@@ -13243,25 +13360,43 @@ async function prepareFromSource(opts) {
|
|
|
13243
13360
|
console.log(" Building release dist (compiles the dashboard \u2014 needs RAM/CPU)\u2026");
|
|
13244
13361
|
await run(
|
|
13245
13362
|
"bun",
|
|
13246
|
-
["run",
|
|
13363
|
+
["run", join4(sourceDir, "apps/api/scripts/build-release.ts")],
|
|
13247
13364
|
sourceDir,
|
|
13248
13365
|
{ DIST_DIR: distDir, NODE_ENV: "production", CLOUD_MODE: "false", OPENSHIP_TARGET: "local" }
|
|
13249
13366
|
);
|
|
13250
13367
|
console.log(" Installing runtime dependencies in the dist\u2026");
|
|
13251
13368
|
await run("bun", ["install", "--production", "--frozen-lockfile"], distDir);
|
|
13252
|
-
const apiDir =
|
|
13253
|
-
const dashboardDir =
|
|
13254
|
-
if (!existsSync2(
|
|
13369
|
+
const apiDir = join4(distDir, "api");
|
|
13370
|
+
const dashboardDir = join4(distDir, "dashboard");
|
|
13371
|
+
if (!existsSync2(join4(apiDir, "src/index.ts"))) {
|
|
13255
13372
|
throw new Error(`Build produced no API at ${apiDir}/src/index.ts \u2014 build-release layout drift?`);
|
|
13256
13373
|
}
|
|
13257
|
-
if (!existsSync2(
|
|
13374
|
+
if (!existsSync2(join4(dashboardDir, "apps/dashboard/server.js"))) {
|
|
13258
13375
|
throw new Error(`Build produced no dashboard at ${dashboardDir}/apps/dashboard/server.js.`);
|
|
13259
13376
|
}
|
|
13260
13377
|
return { apiDir, dashboardDir, ref, sha, sourceDir };
|
|
13261
13378
|
}
|
|
13262
13379
|
|
|
13380
|
+
// src/lib/compose.ts
|
|
13381
|
+
import { spawn as spawn2, spawnSync as spawnSync4 } from "child_process";
|
|
13382
|
+
import { randomBytes as randomBytes5 } from "crypto";
|
|
13383
|
+
import {
|
|
13384
|
+
chmodSync,
|
|
13385
|
+
existsSync as existsSync4,
|
|
13386
|
+
mkdirSync as mkdirSync3,
|
|
13387
|
+
readFileSync as readFileSync3,
|
|
13388
|
+
renameSync,
|
|
13389
|
+
rmSync,
|
|
13390
|
+
writeFileSync as writeFileSync2
|
|
13391
|
+
} from "fs";
|
|
13392
|
+
import { homedir, userInfo } from "os";
|
|
13393
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
13394
|
+
|
|
13263
13395
|
// src/lib/source-install.ts
|
|
13264
|
-
|
|
13396
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
13397
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
13398
|
+
import { join as join5 } from "path";
|
|
13399
|
+
var MARKER = join5(OS_DIR, "source-install.json");
|
|
13265
13400
|
function parseSourceInstall(raw) {
|
|
13266
13401
|
try {
|
|
13267
13402
|
const obj = JSON.parse(raw);
|
|
@@ -13284,7 +13419,7 @@ function parseRemoteSha(stdout) {
|
|
|
13284
13419
|
return sha && /^[0-9a-f]{7,40}$/i.test(sha) ? sha.slice(0, 7) : null;
|
|
13285
13420
|
}
|
|
13286
13421
|
function remoteSha(repo, ref) {
|
|
13287
|
-
const r2 =
|
|
13422
|
+
const r2 = spawnSync3("git", ["ls-remote", repo, ref], { encoding: "utf8" });
|
|
13288
13423
|
if (r2.status !== 0 || !r2.stdout) return null;
|
|
13289
13424
|
return parseRemoteSha(r2.stdout);
|
|
13290
13425
|
}
|
|
@@ -13295,7 +13430,7 @@ async function rebuildFromSource(info) {
|
|
|
13295
13430
|
if (!has("git")) {
|
|
13296
13431
|
throw new Error("`git` is required to update the source checkout but wasn't found on PATH.");
|
|
13297
13432
|
}
|
|
13298
|
-
if (!existsSync3(
|
|
13433
|
+
if (!existsSync3(join5(info.dir, ".git"))) {
|
|
13299
13434
|
throw new Error(
|
|
13300
13435
|
`Source checkout ${info.dir} is missing (no .git) \u2014 reinstall from source with scripts/install-source.sh.`
|
|
13301
13436
|
);
|
|
@@ -13304,7 +13439,7 @@ async function rebuildFromSource(info) {
|
|
|
13304
13439
|
await run("git", ["checkout", info.ref], info.dir);
|
|
13305
13440
|
await run("git", ["pull", "--ff-only", "origin", info.ref], info.dir).catch(() => {
|
|
13306
13441
|
});
|
|
13307
|
-
const cliDir =
|
|
13442
|
+
const cliDir = join5(info.dir, "apps/cli");
|
|
13308
13443
|
await run("bun", ["install"], info.dir);
|
|
13309
13444
|
await run("bun", ["run", "build"], cliDir);
|
|
13310
13445
|
await run("bun", ["run", "build/stage-dashboard.ts"], cliDir);
|
|
@@ -13317,11 +13452,13 @@ var EDGE_ACME_HOST_DIR = `${EDGE_HOST_STATE_DIR}/acme`;
|
|
|
13317
13452
|
function edgeVolumeYaml(indent) {
|
|
13318
13453
|
return EDGE_CONTAINER_MOUNTS.map((m2) => `${indent}- ${m2.host}:${m2.container}:z`).join("\n");
|
|
13319
13454
|
}
|
|
13320
|
-
var COMPOSE_DIR =
|
|
13321
|
-
var INSTALL_METHOD_FILE =
|
|
13322
|
-
var COMPOSE_FILE =
|
|
13323
|
-
var BUILD_FILE =
|
|
13324
|
-
var ENV_FILE =
|
|
13455
|
+
var COMPOSE_DIR = join6(OS_DIR, "compose");
|
|
13456
|
+
var INSTALL_METHOD_FILE = join6(OS_DIR, "install-method");
|
|
13457
|
+
var COMPOSE_FILE = join6(COMPOSE_DIR, "docker-compose.yml");
|
|
13458
|
+
var BUILD_FILE = join6(COMPOSE_DIR, "docker-compose.build.yml");
|
|
13459
|
+
var ENV_FILE = join6(COMPOSE_DIR, ".env");
|
|
13460
|
+
var ENV_BACKUP_FILE = join6(COMPOSE_DIR, ".env.bak");
|
|
13461
|
+
var ENV_TMP_FILE = join6(COMPOSE_DIR, ".env.tmp");
|
|
13325
13462
|
var BUILT_SERVICES = [
|
|
13326
13463
|
{ service: "api", dockerfile: "apps/api/Dockerfile" },
|
|
13327
13464
|
{ service: "dashboard", dockerfile: "apps/dashboard/Dockerfile" },
|
|
@@ -13341,7 +13478,7 @@ function writeInstallMethod(method) {
|
|
|
13341
13478
|
writeFileSync2(INSTALL_METHOD_FILE, method, { mode: 384 });
|
|
13342
13479
|
}
|
|
13343
13480
|
function dockerState() {
|
|
13344
|
-
const ok = (args) =>
|
|
13481
|
+
const ok = (args) => spawnSync4("docker", args, { stdio: "ignore" }).status === 0;
|
|
13345
13482
|
if (!ok(["--version"])) return { binary: false, plugin: false, daemon: false };
|
|
13346
13483
|
return { binary: true, plugin: ok(["compose", "version"]), daemon: ok(["version"]) };
|
|
13347
13484
|
}
|
|
@@ -13356,13 +13493,13 @@ function dockerGap(state = dockerState()) {
|
|
|
13356
13493
|
};
|
|
13357
13494
|
}
|
|
13358
13495
|
if (!state.daemon) {
|
|
13359
|
-
const
|
|
13496
|
+
const start = pasteable(thisHost().dockerStart());
|
|
13360
13497
|
return {
|
|
13361
13498
|
summary: "Docker is installed but its daemon isn't reachable",
|
|
13362
13499
|
// Reinstalling changes nothing: a group change only applies to NEW logins,
|
|
13363
13500
|
// and a stopped daemon needs starting, not installing.
|
|
13364
13501
|
installable: false,
|
|
13365
|
-
hint:
|
|
13502
|
+
hint: thisHost().profile.isRoot ? start ? `Start it with: ${start}` : "Start the Docker daemon \u2014 this host has no service manager Openship recognizes." : `Add your user to the docker group: sudo usermod -aG docker ${userInfo().username} \u2014 then log out and back in (or run: newgrp docker). If the daemon is stopped: ${start ?? "start it the way this host starts services"}`
|
|
13366
13503
|
};
|
|
13367
13504
|
}
|
|
13368
13505
|
return null;
|
|
@@ -13371,7 +13508,7 @@ function hasDockerCompose() {
|
|
|
13371
13508
|
return dockerGap() === null;
|
|
13372
13509
|
}
|
|
13373
13510
|
function hasCmd(cmd) {
|
|
13374
|
-
return
|
|
13511
|
+
return spawnSync4(cmd, ["--version"], { stdio: "ignore" }).status === 0;
|
|
13375
13512
|
}
|
|
13376
13513
|
function shQuote(s2) {
|
|
13377
13514
|
return `'${s2.replace(/'/g, `'\\''`)}'`;
|
|
@@ -13380,23 +13517,22 @@ function dockerInstallPreview(state = dockerState()) {
|
|
|
13380
13517
|
const gap = dockerGap(state);
|
|
13381
13518
|
if (!gap) return { state, gap: null, wouldInstall: false };
|
|
13382
13519
|
if (process.platform !== "linux" || !gap.installable) return { state, gap, wouldInstall: false };
|
|
13383
|
-
const plan = systemCatalog.installs.docker(
|
|
13384
|
-
|
|
13385
|
-
|
|
13386
|
-
});
|
|
13387
|
-
if (!plan.supported || !plan.installCommand) return { state, gap, wouldInstall: false };
|
|
13520
|
+
const plan = systemCatalog.installs.docker(resolveLocalEnvironmentSync());
|
|
13521
|
+
if (!plan.supported) return { state, gap, wouldInstall: false, unsupportedReason: plan.reason };
|
|
13522
|
+
const start = plan.value.service;
|
|
13388
13523
|
return {
|
|
13389
13524
|
state,
|
|
13390
13525
|
gap,
|
|
13391
13526
|
wouldInstall: true,
|
|
13392
|
-
installCommand: plan.installCommand,
|
|
13393
|
-
...
|
|
13527
|
+
installCommand: plan.value.installCommand,
|
|
13528
|
+
...start?.supported ? { startCommand: start.value } : {},
|
|
13529
|
+
...start && !start.supported ? { startBlockedReason: start.reason } : {}
|
|
13394
13530
|
};
|
|
13395
13531
|
}
|
|
13396
13532
|
async function ensureDocker(opts = {}) {
|
|
13397
13533
|
const notice = opts.onNotice ?? ((line) => process.stderr.write(` ${line}
|
|
13398
13534
|
`));
|
|
13399
|
-
const { state, gap, wouldInstall, installCommand, startCommand } = dockerInstallPreview();
|
|
13535
|
+
const { state, gap, wouldInstall, installCommand, startCommand, unsupportedReason, startBlockedReason } = dockerInstallPreview();
|
|
13400
13536
|
if (!gap) return true;
|
|
13401
13537
|
if (process.platform !== "linux") return false;
|
|
13402
13538
|
if (!gap.installable) {
|
|
@@ -13404,14 +13540,17 @@ async function ensureDocker(opts = {}) {
|
|
|
13404
13540
|
if (gap.hint) notice(gap.hint);
|
|
13405
13541
|
return false;
|
|
13406
13542
|
}
|
|
13407
|
-
if (!wouldInstall || !installCommand)
|
|
13543
|
+
if (!wouldInstall || !installCommand) {
|
|
13544
|
+
if (unsupportedReason) notice(unsupportedReason);
|
|
13545
|
+
return false;
|
|
13546
|
+
}
|
|
13408
13547
|
const asRoot = typeof process.getuid === "function" && process.getuid() === 0;
|
|
13409
13548
|
const sudo = !asRoot && hasCmd("sudo") ? "sudo " : "";
|
|
13410
13549
|
notice("This can take 2-5 minutes on a small VPS (~150 MB of packages) and stays quiet while apt works.");
|
|
13411
13550
|
if (state.binary) {
|
|
13412
13551
|
notice("Docker's installer will warn that docker already exists and pause ~20s before continuing \u2014 that's expected.");
|
|
13413
13552
|
}
|
|
13414
|
-
const sh = (script) => new Promise((
|
|
13553
|
+
const sh = (script) => new Promise((resolve3) => {
|
|
13415
13554
|
const child = spawn2("sh", ["-c", sudo ? `${sudo}sh -c ${shQuote(script)}` : script], {
|
|
13416
13555
|
stdio: "inherit"
|
|
13417
13556
|
});
|
|
@@ -13422,13 +13561,14 @@ async function ensureDocker(opts = {}) {
|
|
|
13422
13561
|
}, 3e4);
|
|
13423
13562
|
const done = (code) => {
|
|
13424
13563
|
clearInterval(tick);
|
|
13425
|
-
|
|
13564
|
+
resolve3(code);
|
|
13426
13565
|
};
|
|
13427
13566
|
child.on("error", () => done(1));
|
|
13428
13567
|
child.on("close", (code) => done(code ?? 1));
|
|
13429
13568
|
});
|
|
13430
13569
|
if (await sh(installCommand) !== 0) return false;
|
|
13431
13570
|
if (startCommand) await sh(startCommand);
|
|
13571
|
+
else if (startBlockedReason) notice(startBlockedReason);
|
|
13432
13572
|
const after = dockerGap();
|
|
13433
13573
|
if (!after) {
|
|
13434
13574
|
notice("Docker ready.");
|
|
@@ -13481,7 +13621,14 @@ services:
|
|
|
13481
13621
|
# (set by \`openship up\` when a public URL is configured for off-box access).
|
|
13482
13622
|
ports: ["\${OPENSHIP_BIND_ADDR:-127.0.0.1}:\${API_PORT:-4000}:\${API_PORT:-4000}"]
|
|
13483
13623
|
volumes:
|
|
13484
|
-
|
|
13624
|
+
# The daemon socket, at the path the api's own default expects INSIDE the
|
|
13625
|
+
# container. The HOST side is a variable because it MOVES: a rootless daemon
|
|
13626
|
+
# keeps its socket under the invoking user's runtime dir, and Docker creates a
|
|
13627
|
+
# missing bind source as an empty DIRECTORY instead of failing \u2014 so the rootful
|
|
13628
|
+
# path there brings the stack up and then fails every container operation in
|
|
13629
|
+
# the transport, naming neither the path nor the cause (#482). \`openship up\`
|
|
13630
|
+
# writes OPENSHIP_DOCKER_SOCKET when the detected path isn't this default.
|
|
13631
|
+
- \${OPENSHIP_DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
|
|
13485
13632
|
# Routing state shared with the edge, as HOST BIND MOUNTS (generated from
|
|
13486
13633
|
# EDGE_CONTAINER_MOUNTS): the vhost tree, /etc/letsencrypt, the ACME webroot
|
|
13487
13634
|
# and the static doc-roots the API writes and the edge serves. Named volumes
|
|
@@ -13490,9 +13637,16 @@ services:
|
|
|
13490
13637
|
${edgeVolumeYaml(" ")}
|
|
13491
13638
|
# Host-op SSH key (createHostExecutor \u2192 host.docker.internal). /dev/null
|
|
13492
13639
|
# when the host channel isn't provisioned \u2192 OPENSHIP_HOST_SSH_HOST stays
|
|
13493
|
-
# unset and
|
|
13640
|
+
# unset, and host operations REFUSE (they'd otherwise run against this
|
|
13641
|
+
# container's filesystem, #509) while ordinary deploys keep working over
|
|
13642
|
+
# the socket below.
|
|
13494
13643
|
- \${OPENSHIP_HOST_KEY_PATH:-/dev/null}:/run/secrets/openship_host_key:ro
|
|
13495
|
-
|
|
13644
|
+
# \`host-gateway\` is resolved by the daemon and is right on a stock rootful
|
|
13645
|
+
# install. It is NOT universally right: under rootless Docker it lands in
|
|
13646
|
+
# RootlessKit's namespace rather than the host's, so the host's sshd isn't
|
|
13647
|
+
# there (#482). OPENSHIP_HOST_GATEWAY re-points the alias (e.g. at the box's
|
|
13648
|
+
# LAN address) without patching this file, which \`openship up\` rewrites.
|
|
13649
|
+
extra_hosts: ["host.docker.internal:\${OPENSHIP_HOST_GATEWAY:-host-gateway}"]
|
|
13496
13650
|
env_file: [.env]
|
|
13497
13651
|
environment:
|
|
13498
13652
|
NODE_ENV: production
|
|
@@ -13546,97 +13700,437 @@ volumes:
|
|
|
13546
13700
|
function keepSecret(existing, key) {
|
|
13547
13701
|
return existing[key] || randomBytes5(32).toString("hex");
|
|
13548
13702
|
}
|
|
13703
|
+
var GENERATED_SECRET_KEYS = ["POSTGRES_PASSWORD", "BETTER_AUTH_SECRET", "INTERNAL_TOKEN"];
|
|
13704
|
+
function survivingDbVolume(prev) {
|
|
13705
|
+
const candidates = /* @__PURE__ */ new Set([prev.COMPOSE_PROJECT_NAME, "openship", "compose"]);
|
|
13706
|
+
for (const project of candidates) {
|
|
13707
|
+
if (project && dbVolumeExists(project)) return `${project}_postgres_data`;
|
|
13708
|
+
}
|
|
13709
|
+
return null;
|
|
13710
|
+
}
|
|
13711
|
+
function secretRotationRisk(prev) {
|
|
13712
|
+
const keys = GENERATED_SECRET_KEYS.filter((k2) => !prev[k2]);
|
|
13713
|
+
if (keys.length === 0) return null;
|
|
13714
|
+
const volume = survivingDbVolume(prev);
|
|
13715
|
+
return volume ? { volume, keys: [...keys] } : null;
|
|
13716
|
+
}
|
|
13717
|
+
function composeSecretRotationRisk(opts = {}) {
|
|
13718
|
+
if (opts.resetSecrets) return null;
|
|
13719
|
+
return secretRotationRisk(readEnvFile());
|
|
13720
|
+
}
|
|
13721
|
+
function renderSecretRotationRefusal(rotation, opts = {}) {
|
|
13722
|
+
return `
|
|
13723
|
+
${opts.dryRun ? "A real run would REFUSE here" : "Refusing to continue"}: this would regenerate secrets your existing install already has.
|
|
13724
|
+
|
|
13725
|
+
missing from ${ENV_FILE}: ${rotation.keys.join(", ")}
|
|
13726
|
+
but this install's data is still here: ${rotation.volume}
|
|
13727
|
+
|
|
13728
|
+
A new POSTGRES_PASSWORD cannot match that volume \u2014 Postgres only applies the password
|
|
13729
|
+
when it first creates the data directory \u2014 and a new BETTER_AUTH_SECRET cannot decrypt
|
|
13730
|
+
the environment variables already stored in the database. Nothing has been written yet.
|
|
13731
|
+
|
|
13732
|
+
` + secretRecoveryAdvice(rotation);
|
|
13733
|
+
}
|
|
13734
|
+
function secretRecoveryAdvice(rotation) {
|
|
13735
|
+
const adopted = adoptedStack(projectOfDbVolume(rotation.volume));
|
|
13736
|
+
if (adopted) {
|
|
13737
|
+
return ` This stack was not created by \`openship up\`: it is already running under the compose
|
|
13738
|
+
project name we pin, so this run would ADOPT it.
|
|
13739
|
+
|
|
13740
|
+
project: ${adopted.project}
|
|
13741
|
+
started from: ${adopted.configFiles || "an unlabelled `docker run`"}
|
|
13742
|
+
not from: ${COMPOSE_FILE}
|
|
13743
|
+
|
|
13744
|
+
So there is no \`.env\` of ours to restore and no backup of one \u2014 the real values are in
|
|
13745
|
+
the api container's own environment. Copy them out of it, then re-run:
|
|
13746
|
+
|
|
13747
|
+
echo 'COMPOSE_PROJECT_NAME=${adopted.project}' >> ${ENV_FILE}
|
|
13748
|
+
docker inspect ${adopted.apiContainer} --format '{{range .Config.Env}}{{println .}}{{end}}' \\
|
|
13749
|
+
| grep -E '^(${rotation.keys.join("|")})=' >> ${ENV_FILE}
|
|
13750
|
+
|
|
13751
|
+
COMPOSE_PROJECT_NAME matters as much as the secrets do: it is the volume prefix, so
|
|
13752
|
+
without it this stack comes up on empty volumes beside the data above.
|
|
13753
|
+
|
|
13754
|
+
Only if that container is gone and the values exist nowhere else \u2014 this DESTROYS them,
|
|
13755
|
+
leaving every environment variable stored in the database permanently unreadable:
|
|
13756
|
+
openship up --reset-secrets
|
|
13757
|
+
`;
|
|
13758
|
+
}
|
|
13759
|
+
return ` Put the original values back and re-run:
|
|
13760
|
+
` + (existsSync4(ENV_BACKUP_FILE) ? ` cp ${ENV_BACKUP_FILE} ${ENV_FILE} # the .env a previous \`openship up\` replaced
|
|
13761
|
+
` : ` restore ${ENV_FILE} from a backup, or copy the keys above out of one
|
|
13762
|
+
`) + ` If the file is there but this user can't read it, re-run as the user that owns it
|
|
13763
|
+
(or as root) rather than letting the secrets be regenerated.
|
|
13764
|
+
|
|
13765
|
+
To reset instead \u2014 the database password gets realigned, but every stored environment
|
|
13766
|
+
variable becomes permanently unreadable:
|
|
13767
|
+
openship up --reset-secrets
|
|
13768
|
+
`;
|
|
13769
|
+
}
|
|
13770
|
+
function projectOfDbVolume(volume) {
|
|
13771
|
+
return volume.replace(/_postgres_data$/, "");
|
|
13772
|
+
}
|
|
13549
13773
|
function readEnvFile() {
|
|
13550
13774
|
const out = {};
|
|
13775
|
+
let text;
|
|
13551
13776
|
try {
|
|
13552
|
-
|
|
13553
|
-
|
|
13554
|
-
|
|
13777
|
+
text = readFileSync3(ENV_FILE, "utf8");
|
|
13778
|
+
} catch (err) {
|
|
13779
|
+
if (err.code !== "ENOENT") {
|
|
13780
|
+
console.log(
|
|
13781
|
+
` ! Could not read ${ENV_FILE}: ${err.message}
|
|
13782
|
+
Its contents are being treated as absent. If this install already exists, fix the permissions and re-run rather than letting secrets be regenerated.`
|
|
13783
|
+
);
|
|
13555
13784
|
}
|
|
13556
|
-
|
|
13785
|
+
return out;
|
|
13786
|
+
}
|
|
13787
|
+
for (const line of text.split("\n")) {
|
|
13788
|
+
const m2 = line.match(/^([A-Z0-9_]+)=(.*)$/);
|
|
13789
|
+
if (m2) out[m2[1]] = m2[2];
|
|
13557
13790
|
}
|
|
13558
13791
|
return out;
|
|
13559
13792
|
}
|
|
13793
|
+
var HOST_CHANNEL_DEFAULT_HOST = "host.docker.internal";
|
|
13794
|
+
var HOST_CHANNEL_DEFAULT_PORT = 22;
|
|
13560
13795
|
var HOST_KEY_COMMENT = "openship-host-executor";
|
|
13561
13796
|
var HOST_KEY_FROM = "172.16.0.0/12,192.168.0.0/16,10.0.0.0/8,127.0.0.1";
|
|
13562
13797
|
function hostKeyAuthLine(pub) {
|
|
13563
13798
|
return `from="${HOST_KEY_FROM}",restrict,pty ${pub}`;
|
|
13564
13799
|
}
|
|
13565
13800
|
function rewriteHostAuthorizedKeys(existing, pub) {
|
|
13566
|
-
|
|
13567
|
-
|
|
13801
|
+
return [...keptForeignKeys(existing), hostKeyAuthLine(pub)].join("\n") + "\n";
|
|
13802
|
+
}
|
|
13803
|
+
function keptForeignKeys(existing) {
|
|
13804
|
+
return existing.split("\n").filter((line) => line.trim() && !line.includes(HOST_KEY_COMMENT));
|
|
13805
|
+
}
|
|
13806
|
+
function stripHostAuthorizedKeys(existing) {
|
|
13807
|
+
const kept = keptForeignKeys(existing);
|
|
13808
|
+
return kept.length ? kept.join("\n") + "\n" : "";
|
|
13809
|
+
}
|
|
13810
|
+
var ROOT_AUTHORIZED_KEYS = "/root/.ssh/authorized_keys";
|
|
13811
|
+
function chooseHostChannelUser(input) {
|
|
13812
|
+
if (input.invokerUid === 0) {
|
|
13813
|
+
return { user: "root", authKeysPath: ROOT_AUTHORIZED_KEYS, viaSudo: false, rootUnavailable: false };
|
|
13814
|
+
}
|
|
13815
|
+
if (input.hasPasswordlessSudo) {
|
|
13816
|
+
return { user: "root", authKeysPath: ROOT_AUTHORIZED_KEYS, viaSudo: true, rootUnavailable: false };
|
|
13817
|
+
}
|
|
13818
|
+
return {
|
|
13819
|
+
user: input.invokerName,
|
|
13820
|
+
authKeysPath: join6(input.invokerHome, ".ssh", "authorized_keys"),
|
|
13821
|
+
viaSudo: false,
|
|
13822
|
+
rootUnavailable: true
|
|
13823
|
+
};
|
|
13824
|
+
}
|
|
13825
|
+
function hasPasswordlessSudo() {
|
|
13826
|
+
try {
|
|
13827
|
+
return spawnSync4("sudo", ["-n", "true"], { stdio: "ignore" }).status === 0;
|
|
13828
|
+
} catch {
|
|
13829
|
+
return false;
|
|
13830
|
+
}
|
|
13568
13831
|
}
|
|
13569
13832
|
function plannedHostChannel(hostControl) {
|
|
13570
13833
|
if (!hostControl || process.platform !== "linux") return null;
|
|
13571
|
-
let
|
|
13834
|
+
let invoker;
|
|
13572
13835
|
try {
|
|
13573
|
-
|
|
13836
|
+
const info = userInfo();
|
|
13837
|
+
invoker = { uid: info.uid, username: info.username, home: info.homedir };
|
|
13838
|
+
} catch {
|
|
13839
|
+
invoker = { uid: -1, username: process.env.USER || process.env.LOGNAME || "root", home: homedir() };
|
|
13840
|
+
}
|
|
13841
|
+
const decision = chooseHostChannelUser({
|
|
13842
|
+
invokerUid: invoker.uid,
|
|
13843
|
+
invokerName: invoker.username,
|
|
13844
|
+
invokerHome: invoker.home,
|
|
13845
|
+
// Only probe sudo when it could change the outcome — a root invoker never needs it.
|
|
13846
|
+
hasPasswordlessSudo: invoker.uid !== 0 && hasPasswordlessSudo()
|
|
13847
|
+
});
|
|
13848
|
+
return { ...decision, keyPath: join6(COMPOSE_DIR, "host-ssh", "id_ed25519") };
|
|
13849
|
+
}
|
|
13850
|
+
function authorizeKeyAt(authKeysPath, pub) {
|
|
13851
|
+
mkdirSync3(dirname2(authKeysPath), { recursive: true, mode: 448 });
|
|
13852
|
+
const existing = existsSync4(authKeysPath) ? readFileSync3(authKeysPath, "utf8") : "";
|
|
13853
|
+
const next = rewriteHostAuthorizedKeys(existing, pub);
|
|
13854
|
+
if (next !== existing) writeFileSync2(authKeysPath, next, { mode: 384 });
|
|
13855
|
+
chmodSync(authKeysPath, 384);
|
|
13856
|
+
}
|
|
13857
|
+
function authorizeRootKeyViaSudo(pub) {
|
|
13858
|
+
const mk = spawnSync4("sudo", ["-n", "sh", "-c", "mkdir -p /root/.ssh && chmod 700 /root/.ssh"], {
|
|
13859
|
+
stdio: "ignore"
|
|
13860
|
+
});
|
|
13861
|
+
if (mk.status !== 0) return false;
|
|
13862
|
+
const read = spawnSync4(
|
|
13863
|
+
"sudo",
|
|
13864
|
+
["-n", "sh", "-c", `cat ${ROOT_AUTHORIZED_KEYS} 2>/dev/null || true`],
|
|
13865
|
+
{ encoding: "utf8" }
|
|
13866
|
+
);
|
|
13867
|
+
if (read.status !== 0) return false;
|
|
13868
|
+
const next = rewriteHostAuthorizedKeys(read.stdout ?? "", pub);
|
|
13869
|
+
const write = spawnSync4("sudo", ["-n", "tee", ROOT_AUTHORIZED_KEYS], {
|
|
13870
|
+
input: next,
|
|
13871
|
+
stdio: ["pipe", "ignore", "ignore"]
|
|
13872
|
+
});
|
|
13873
|
+
if (write.status !== 0) return false;
|
|
13874
|
+
return spawnSync4("sudo", ["-n", "chmod", "600", ROOT_AUTHORIZED_KEYS], { stdio: "ignore" }).status === 0;
|
|
13875
|
+
}
|
|
13876
|
+
function revokeKeyAt(authKeysPath) {
|
|
13877
|
+
try {
|
|
13878
|
+
if (!existsSync4(authKeysPath)) return true;
|
|
13879
|
+
const existing = readFileSync3(authKeysPath, "utf8");
|
|
13880
|
+
if (!existing.includes(HOST_KEY_COMMENT)) return true;
|
|
13881
|
+
writeFileSync2(authKeysPath, stripHostAuthorizedKeys(existing), { mode: 384 });
|
|
13882
|
+
chmodSync(authKeysPath, 384);
|
|
13883
|
+
return true;
|
|
13884
|
+
} catch {
|
|
13885
|
+
return false;
|
|
13886
|
+
}
|
|
13887
|
+
}
|
|
13888
|
+
function revokeRootKeyViaSudo() {
|
|
13889
|
+
const read = spawnSync4(
|
|
13890
|
+
"sudo",
|
|
13891
|
+
["-n", "sh", "-c", `cat ${ROOT_AUTHORIZED_KEYS} 2>/dev/null || true`],
|
|
13892
|
+
{ encoding: "utf8" }
|
|
13893
|
+
);
|
|
13894
|
+
if (read.status !== 0) return false;
|
|
13895
|
+
const existing = read.stdout ?? "";
|
|
13896
|
+
if (!existing.includes(HOST_KEY_COMMENT)) return true;
|
|
13897
|
+
const write = spawnSync4("sudo", ["-n", "tee", ROOT_AUTHORIZED_KEYS], {
|
|
13898
|
+
input: stripHostAuthorizedKeys(existing),
|
|
13899
|
+
stdio: ["pipe", "ignore", "ignore"]
|
|
13900
|
+
});
|
|
13901
|
+
return write.status === 0;
|
|
13902
|
+
}
|
|
13903
|
+
function retireHostSshChannel(prev) {
|
|
13904
|
+
const keyDir = join6(COMPOSE_DIR, "host-ssh");
|
|
13905
|
+
if (!existsSync4(keyDir) && !prev.OPENSHIP_HOST_SSH_HOST) return;
|
|
13906
|
+
const invokerKeys = join6(homedir(), ".ssh", "authorized_keys");
|
|
13907
|
+
const asRoot = (prev.OPENSHIP_HOST_SSH_USER?.trim() || "root") === "root";
|
|
13908
|
+
const weAreRoot = typeof process.getuid === "function" && process.getuid() === 0;
|
|
13909
|
+
const revoked = asRoot ? weAreRoot ? revokeKeyAt(ROOT_AUTHORIZED_KEYS) : hasPasswordlessSudo() && revokeRootKeyViaSudo() : revokeKeyAt(invokerKeys);
|
|
13910
|
+
const alsoInvoker = asRoot ? revokeKeyAt(invokerKeys) : true;
|
|
13911
|
+
try {
|
|
13912
|
+
rmSync(keyDir, { recursive: true, force: true });
|
|
13574
13913
|
} catch {
|
|
13575
|
-
user = process.env.USER || process.env.LOGNAME || "root";
|
|
13576
13914
|
}
|
|
13577
|
-
|
|
13915
|
+
const stuck = asRoot ? ROOT_AUTHORIZED_KEYS : invokerKeys;
|
|
13916
|
+
console.log(
|
|
13917
|
+
revoked && alsoInvoker ? " Host control off: dropped OPENSHIP_HOST_SSH_* from .env, deleted the host key and\n revoked its authorized_keys line." : ` Host control off: dropped OPENSHIP_HOST_SSH_* from .env and deleted the host key (it
|
|
13918
|
+
can no longer be used). Removing its line needs root \u2014 run:
|
|
13919
|
+
sudo sed -i '/${HOST_KEY_COMMENT}/d' ${stuck}`
|
|
13920
|
+
);
|
|
13578
13921
|
}
|
|
13579
|
-
function provisionHostSshChannel(
|
|
13580
|
-
const target = plannedHostChannel(hostControl);
|
|
13581
|
-
if (!target) return null;
|
|
13582
|
-
const { user, keyPath } = target;
|
|
13922
|
+
function provisionHostSshChannel(cfg) {
|
|
13923
|
+
const target = plannedHostChannel(cfg.hostControl);
|
|
13924
|
+
if (!target) return { channel: null };
|
|
13925
|
+
const { user, keyPath, authKeysPath, viaSudo, rootUnavailable } = target;
|
|
13926
|
+
const provisioned = (channel) => {
|
|
13927
|
+
const port = toPort(cfg.hostSshPort) ?? HOST_CHANNEL_DEFAULT_PORT;
|
|
13928
|
+
return probeHostSshd(port) === "absent" ? { channel, issue: { code: "no-sshd", detail: `port ${port}` } } : { channel };
|
|
13929
|
+
};
|
|
13583
13930
|
try {
|
|
13584
|
-
mkdirSync3(
|
|
13931
|
+
mkdirSync3(join6(COMPOSE_DIR, "host-ssh"), { recursive: true, mode: 448 });
|
|
13585
13932
|
if (!existsSync4(keyPath)) {
|
|
13586
|
-
const g2 =
|
|
13933
|
+
const g2 = spawnSync4(
|
|
13587
13934
|
"ssh-keygen",
|
|
13588
13935
|
["-t", "ed25519", "-N", "", "-q", "-f", keyPath, "-C", HOST_KEY_COMMENT],
|
|
13589
|
-
|
|
13936
|
+
// stderr is captured rather than discarded: it is the only account of why the
|
|
13937
|
+
// channel is missing, and the caller prints it.
|
|
13938
|
+
{ encoding: "utf8", stdio: ["ignore", "ignore", "pipe"] }
|
|
13590
13939
|
);
|
|
13591
|
-
if (g2.status !== 0) return null;
|
|
13940
|
+
if (g2.status !== 0) return { channel: null, issue: { code: "keygen", detail: keygenWhy(g2) } };
|
|
13592
13941
|
}
|
|
13593
13942
|
const pub = readFileSync3(`${keyPath}.pub`, "utf8").trim();
|
|
13594
|
-
if (!pub) return null;
|
|
13595
|
-
|
|
13596
|
-
|
|
13597
|
-
|
|
13598
|
-
|
|
13599
|
-
|
|
13600
|
-
|
|
13601
|
-
|
|
13602
|
-
|
|
13603
|
-
|
|
13604
|
-
|
|
13943
|
+
if (!pub) return { channel: null, issue: { code: "empty-key" } };
|
|
13944
|
+
if (rootUnavailable) {
|
|
13945
|
+
console.warn(
|
|
13946
|
+
" \u26A0 Host control needs root, but this user isn't root and passwordless sudo isn't\n available. Mail and edge host operations will fail. Re-run `openship up` as root\n (or enable passwordless sudo) to fix. Continuing with a limited host channel."
|
|
13947
|
+
);
|
|
13948
|
+
authorizeKeyAt(authKeysPath, pub);
|
|
13949
|
+
return provisioned({ user, keyPath });
|
|
13950
|
+
}
|
|
13951
|
+
if (viaSudo) {
|
|
13952
|
+
if (!authorizeRootKeyViaSudo(pub)) {
|
|
13953
|
+
console.warn(
|
|
13954
|
+
" \u26A0 Could not authorize the host key for root via sudo \u2014 falling back to the\n current user. Root host operations (mail/edge) may fail."
|
|
13955
|
+
);
|
|
13956
|
+
const invoker = userInfo().username;
|
|
13957
|
+
authorizeKeyAt(join6(homedir(), ".ssh", "authorized_keys"), pub);
|
|
13958
|
+
return provisioned({ user: invoker, keyPath });
|
|
13959
|
+
}
|
|
13960
|
+
revokeKeyAt(join6(homedir(), ".ssh", "authorized_keys"));
|
|
13961
|
+
return provisioned({ user, keyPath });
|
|
13962
|
+
}
|
|
13963
|
+
authorizeKeyAt(authKeysPath, pub);
|
|
13964
|
+
return provisioned({ user, keyPath });
|
|
13965
|
+
} catch (err) {
|
|
13966
|
+
return { channel: null, issue: { code: "error", detail: err?.message } };
|
|
13605
13967
|
}
|
|
13606
13968
|
}
|
|
13969
|
+
function keygenWhy(r2) {
|
|
13970
|
+
if (r2.error?.code === "ENOENT") {
|
|
13971
|
+
return "ssh-keygen is not installed \u2014 the openssh-client package provides it";
|
|
13972
|
+
}
|
|
13973
|
+
const stderr = (r2.stderr ?? "").trim().split("\n").pop();
|
|
13974
|
+
return stderr || r2.error?.message || `ssh-keygen exited ${r2.status ?? "abnormally"}`;
|
|
13975
|
+
}
|
|
13976
|
+
function probeHostSshd(port) {
|
|
13977
|
+
for (const [cmd, args] of [
|
|
13978
|
+
["ss", ["-H", "-ltn"]],
|
|
13979
|
+
["netstat", ["-ltn"]]
|
|
13980
|
+
]) {
|
|
13981
|
+
const r2 = spawnSync4(cmd, [...args], { encoding: "utf8" });
|
|
13982
|
+
if (r2.status !== 0 || !r2.stdout) continue;
|
|
13983
|
+
return listensOnPort(r2.stdout, port) ? "listening" : "absent";
|
|
13984
|
+
}
|
|
13985
|
+
return "unknown";
|
|
13986
|
+
}
|
|
13987
|
+
function listensOnPort(listing, port) {
|
|
13988
|
+
return listing.split("\n").filter((line) => /\bLISTEN\b/.test(line)).some(
|
|
13989
|
+
(line) => line.trim().split(/\s+/).some((token) => token.endsWith(`:${port}`))
|
|
13990
|
+
);
|
|
13991
|
+
}
|
|
13992
|
+
function carriedHostChannel(prev) {
|
|
13993
|
+
const keyPath = prev.OPENSHIP_HOST_KEY_PATH?.trim();
|
|
13994
|
+
if (!prev.OPENSHIP_HOST_SSH_HOST?.trim() || !keyPath || !existsSync4(keyPath)) return null;
|
|
13995
|
+
return { user: prev.OPENSHIP_HOST_SSH_USER?.trim() || "root", keyPath };
|
|
13996
|
+
}
|
|
13997
|
+
var HOST_ISSUE_INDENT = " ";
|
|
13998
|
+
function hostIssueNote(text) {
|
|
13999
|
+
return wrapText(text, 80).map((l2) => `${HOST_ISSUE_INDENT}${l2}
|
|
14000
|
+
`).join("");
|
|
14001
|
+
}
|
|
14002
|
+
function renderHostChannelIssue(issue, ctx) {
|
|
14003
|
+
const detail = issue.detail ? ` (${issue.detail})` : "";
|
|
14004
|
+
const headline = issue.code === "no-sshd" ? ` \u26A0 Host control is provisioned, but nothing on this host is listening for SSH${detail}.` : ` \u26A0 Host control could NOT be provisioned on this box.`;
|
|
14005
|
+
let cause;
|
|
14006
|
+
switch (issue.code) {
|
|
14007
|
+
case "keygen":
|
|
14008
|
+
cause = `The channel's key could not be generated${detail}.`;
|
|
14009
|
+
break;
|
|
14010
|
+
case "empty-key":
|
|
14011
|
+
cause = "The channel's key was generated but its public half read back empty.";
|
|
14012
|
+
break;
|
|
14013
|
+
case "no-sshd":
|
|
14014
|
+
cause = `The api container now has a key and an address, and will be refused the first time it dials ${ctx.target}:
|
|
14015
|
+
` + explainHostChannelCause("refused", {
|
|
14016
|
+
target: ctx.target,
|
|
14017
|
+
// Measured here rather than left to core's default, which is Debian's `ssh` —
|
|
14018
|
+
// a unit name that does not exist on the RHEL family, so the remedy printed on
|
|
14019
|
+
// an Amazon Linux box was a command that silently does nothing.
|
|
14020
|
+
sshdEnableHint: thisHost().sshdEnableHint()
|
|
14021
|
+
}).body;
|
|
14022
|
+
break;
|
|
14023
|
+
case "error":
|
|
14024
|
+
cause = `Setting up the channel's key failed${detail}.`;
|
|
14025
|
+
break;
|
|
14026
|
+
}
|
|
14027
|
+
const continuity = issue.code === "no-sshd" ? "" : ctx.kept ? hostIssueNote(
|
|
14028
|
+
"Kept the host channel this install already had in `.env` rather than dropping it: its key is still authorized, so the api container keeps the access it had instead of losing it to this failure."
|
|
14029
|
+
) : hostIssueNote(
|
|
14030
|
+
"OPENSHIP_HOST_SSH_HOST is therefore unset, and host operations refuse rather than run against the api container's own filesystem."
|
|
14031
|
+
);
|
|
14032
|
+
return `${headline}
|
|
14033
|
+
` + hostIssueNote(cause) + continuity + hostIssueNote(HOST_CHANNEL_UNAFFECTED) + `${HOST_ISSUE_INDENT}Unavailable until the channel works:
|
|
14034
|
+
` + HOST_CHANNEL_BLOCKED.map((l2) => `${HOST_ISSUE_INDENT} \xB7 ${l2}
|
|
14035
|
+
`).join("") + hostIssueNote(HOST_CHANNEL_RECHECK);
|
|
14036
|
+
}
|
|
13607
14037
|
function composeProjectName(prev) {
|
|
13608
14038
|
if (prev.COMPOSE_PROJECT_NAME) return prev.COMPOSE_PROJECT_NAME;
|
|
13609
14039
|
return Object.keys(prev).length > 0 ? "compose" : "openship";
|
|
13610
14040
|
}
|
|
13611
14041
|
var PGDATA_ROOT = "/var/lib/postgresql/data";
|
|
13612
|
-
function
|
|
13613
|
-
|
|
13614
|
-
return dbVolumeExists(composeProjectName(prev)) ? PGDATA_ROOT : `${PGDATA_ROOT}/pgdata`;
|
|
13615
|
-
}
|
|
13616
|
-
function orphanedStackContainers(project) {
|
|
13617
|
-
const r2 = spawnSync3(
|
|
14042
|
+
function probePgDataDir(volume) {
|
|
14043
|
+
const r2 = spawnSync4(
|
|
13618
14044
|
"docker",
|
|
13619
14045
|
[
|
|
13620
|
-
"
|
|
13621
|
-
"
|
|
13622
|
-
"
|
|
13623
|
-
|
|
14046
|
+
"run",
|
|
14047
|
+
"--rm",
|
|
14048
|
+
"-v",
|
|
14049
|
+
`${volume}:/from:ro`,
|
|
14050
|
+
"alpine:3",
|
|
14051
|
+
"sh",
|
|
14052
|
+
"-c",
|
|
14053
|
+
`if [ -f /from/PG_VERSION ]; then echo root; elif [ -f /from/pgdata/PG_VERSION ]; then echo sub; elif [ -z "$(ls -A /from 2>/dev/null | grep -v '^lost+found$')" ]; then echo empty; else echo foreign; fi`
|
|
13624
14054
|
],
|
|
13625
14055
|
{ encoding: "utf8" }
|
|
13626
14056
|
);
|
|
13627
|
-
if (r2.status !== 0
|
|
13628
|
-
|
|
13629
|
-
|
|
13630
|
-
|
|
13631
|
-
|
|
13632
|
-
|
|
13633
|
-
|
|
13634
|
-
|
|
14057
|
+
if (r2.status !== 0) return "unknown";
|
|
14058
|
+
const out = `${r2.stdout ?? ""}`.trim();
|
|
14059
|
+
return out === "root" || out === "sub" || out === "empty" || out === "foreign" ? out : "unknown";
|
|
14060
|
+
}
|
|
14061
|
+
function resolvePgData(prev) {
|
|
14062
|
+
const SUB = `${PGDATA_ROOT}/pgdata`;
|
|
14063
|
+
if (prev.OPENSHIP_PGDATA) return prev.OPENSHIP_PGDATA;
|
|
14064
|
+
const volume = survivingDbVolume(prev);
|
|
14065
|
+
if (!volume) return SUB;
|
|
14066
|
+
return probePgDataDir(volume) === "root" ? PGDATA_ROOT : SUB;
|
|
14067
|
+
}
|
|
14068
|
+
function pgDataDetectionRisk(prev) {
|
|
14069
|
+
if (prev.OPENSHIP_PGDATA) return null;
|
|
14070
|
+
const volume = survivingDbVolume(prev);
|
|
14071
|
+
if (!volume) return null;
|
|
14072
|
+
return probePgDataDir(volume) === "foreign" ? { volume } : null;
|
|
14073
|
+
}
|
|
14074
|
+
function composePgDataRisk(_opts = {}) {
|
|
14075
|
+
return pgDataDetectionRisk(readEnvFile());
|
|
14076
|
+
}
|
|
14077
|
+
function renderPgDataRefusal(risk, opts = {}) {
|
|
14078
|
+
const SUB = `${PGDATA_ROOT}/pgdata`;
|
|
14079
|
+
return `
|
|
14080
|
+
${opts.dryRun ? "A real run would REFUSE here" : "Refusing to continue"}: this install's data volume holds data, but not a Postgres cluster we recognize.
|
|
14081
|
+
|
|
14082
|
+
volume: ${risk.volume}
|
|
14083
|
+
checked: ${PGDATA_ROOT}/PG_VERSION and ${SUB}/PG_VERSION \u2014 neither present
|
|
14084
|
+
|
|
14085
|
+
Guessing where the cluster lives is how a wrong path makes Postgres run initdb over
|
|
14086
|
+
your existing data and orphan it, so nothing has been written. If you know the layout,
|
|
14087
|
+
pin it and re-run:
|
|
14088
|
+
|
|
14089
|
+
echo 'OPENSHIP_PGDATA=${SUB}' >> ${ENV_FILE} # or ${PGDATA_ROOT} if the cluster is at the root
|
|
14090
|
+
`;
|
|
14091
|
+
}
|
|
14092
|
+
var PS_SWEEP_FORMAT = '{{.Names}} {{.Label "com.docker.compose.project"}} {{.Label "com.docker.compose.project.config_files"}} {{.Label "com.docker.compose.service"}}';
|
|
14093
|
+
function parsePsSweep(stdout) {
|
|
14094
|
+
return stdout.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
14095
|
+
const [name, project, configFiles, service] = line.split(" ");
|
|
14096
|
+
return {
|
|
14097
|
+
name: name ?? "",
|
|
14098
|
+
project: project ?? "",
|
|
14099
|
+
configFiles: configFiles ?? "",
|
|
14100
|
+
service: service ?? ""
|
|
14101
|
+
};
|
|
14102
|
+
});
|
|
14103
|
+
}
|
|
14104
|
+
function psSweepRaw() {
|
|
14105
|
+
const r2 = spawnSync4("docker", ["ps", "-a", "--format", PS_SWEEP_FORMAT], { encoding: "utf8" });
|
|
14106
|
+
return r2.status === 0 && r2.stdout ? r2.stdout : null;
|
|
14107
|
+
}
|
|
14108
|
+
function psSweep() {
|
|
14109
|
+
return parsePsSweep(psSweepRaw() ?? "");
|
|
14110
|
+
}
|
|
14111
|
+
function fromOurComposeFile(row) {
|
|
14112
|
+
return row.configFiles.split(",").some((f2) => f2.trim() === COMPOSE_FILE);
|
|
14113
|
+
}
|
|
14114
|
+
function orphanedStackContainers(project) {
|
|
14115
|
+
return psSweep().filter((c2) => c2.name && c2.project && c2.project !== project && fromOurComposeFile(c2)).map(({ name, project: proj }) => ({ name, project: proj }));
|
|
14116
|
+
}
|
|
14117
|
+
function adoptedStackOrigin(psStdout, project) {
|
|
14118
|
+
const rows = parsePsSweep(psStdout).filter((c2) => c2.name && c2.project === project);
|
|
14119
|
+
if (rows.length === 0 || rows.some(fromOurComposeFile)) return null;
|
|
14120
|
+
return {
|
|
14121
|
+
project,
|
|
14122
|
+
configFiles: rows.map((c2) => c2.configFiles).find(Boolean) ?? "",
|
|
14123
|
+
apiContainer: rows.find((c2) => c2.service === "api")?.name || `${project}-api-1`
|
|
14124
|
+
};
|
|
14125
|
+
}
|
|
14126
|
+
function adoptedStack(project) {
|
|
14127
|
+
const sweep = psSweepRaw();
|
|
14128
|
+
return sweep ? adoptedStackOrigin(sweep, project) : null;
|
|
13635
14129
|
}
|
|
13636
14130
|
function reconcileDbPassword(user, password) {
|
|
13637
14131
|
if (!password) return;
|
|
13638
14132
|
if (compose(["up", "-d", "--wait", "postgres"], { quiet: true }) !== 0) return;
|
|
13639
|
-
const attempt = (asRole) =>
|
|
14133
|
+
const attempt = (asRole) => spawnSync4(
|
|
13640
14134
|
"docker",
|
|
13641
14135
|
[
|
|
13642
14136
|
"compose",
|
|
@@ -13698,7 +14192,21 @@ function removeOrphanedStack(project) {
|
|
|
13698
14192
|
console.log(
|
|
13699
14193
|
` Removing ${orphans.length} container(s) from a previous stack (project "${projects}") \u2014 they would hold the ports this stack needs. Volumes are kept.`
|
|
13700
14194
|
);
|
|
13701
|
-
|
|
14195
|
+
spawnSync4("docker", ["rm", "-f", ...orphans.map((o2) => o2.name)], { stdio: "ignore" });
|
|
14196
|
+
}
|
|
14197
|
+
function edgeNameNeedsReclaim(psStdout, project) {
|
|
14198
|
+
const row = parsePsSweep(psStdout).find((c2) => c2.name === "openship-edge");
|
|
14199
|
+
if (!row) return false;
|
|
14200
|
+
return !(row.project === project && fromOurComposeFile(row));
|
|
14201
|
+
}
|
|
14202
|
+
function reconcileEdgeContainerName(project) {
|
|
14203
|
+
const sweep = psSweepRaw();
|
|
14204
|
+
if (!sweep) return;
|
|
14205
|
+
if (!edgeNameNeedsReclaim(sweep, project)) return;
|
|
14206
|
+
console.log(
|
|
14207
|
+
" Replacing an unmanaged `openship-edge` container so compose can own it (config is on the host, so no routes are lost)."
|
|
14208
|
+
);
|
|
14209
|
+
spawnSync4("docker", ["rm", "-f", "openship-edge"], { stdio: "ignore" });
|
|
13702
14210
|
}
|
|
13703
14211
|
function migrateLegacyEdgeVolumes(project) {
|
|
13704
14212
|
const pairs = [
|
|
@@ -13707,18 +14215,18 @@ function migrateLegacyEdgeVolumes(project) {
|
|
|
13707
14215
|
{ volume: `${project}_openship_acme`, host: EDGE_ACME_HOST_DIR },
|
|
13708
14216
|
{ volume: `${project}_openship_static`, host: "/opt/openship/static" }
|
|
13709
14217
|
];
|
|
13710
|
-
const ls =
|
|
14218
|
+
const ls = spawnSync4("docker", ["volume", "ls", "--format", "{{.Name}}"], { encoding: "utf8" });
|
|
13711
14219
|
if (ls.status !== 0 || !ls.stdout) return;
|
|
13712
14220
|
const existing = new Set(ls.stdout.split("\n").map((l2) => l2.trim()).filter(Boolean));
|
|
13713
14221
|
for (const { volume, host } of pairs) {
|
|
13714
14222
|
if (!existing.has(volume)) continue;
|
|
13715
14223
|
mkdirSync3(host, { recursive: true });
|
|
13716
|
-
const alreadyThere =
|
|
14224
|
+
const alreadyThere = spawnSync4("sh", ["-c", `ls -A ${JSON.stringify(host)} 2>/dev/null | head -1`], {
|
|
13717
14225
|
encoding: "utf8"
|
|
13718
14226
|
});
|
|
13719
14227
|
if (alreadyThere.stdout?.trim()) continue;
|
|
13720
14228
|
console.log(` Moving edge state from volume ${volume} onto ${host}...`);
|
|
13721
|
-
const copy =
|
|
14229
|
+
const copy = spawnSync4(
|
|
13722
14230
|
"docker",
|
|
13723
14231
|
[
|
|
13724
14232
|
"run",
|
|
@@ -13743,7 +14251,7 @@ function migrateLegacyEdgeVolumes(project) {
|
|
|
13743
14251
|
}
|
|
13744
14252
|
}
|
|
13745
14253
|
function warnOrphanedVolumes(project) {
|
|
13746
|
-
const r2 =
|
|
14254
|
+
const r2 = spawnSync4("docker", ["volume", "ls", "--format", "{{.Name}}"], { encoding: "utf8" });
|
|
13747
14255
|
if (r2.status !== 0 || !r2.stdout) return;
|
|
13748
14256
|
const others = new Set(
|
|
13749
14257
|
r2.stdout.split("\n").map((l2) => l2.trim()).filter((l2) => l2.endsWith("_openship_sites")).map((l2) => l2.slice(0, -"_openship_sites".length)).filter((p2) => p2 && p2 !== project)
|
|
@@ -13771,6 +14279,52 @@ var ACME_ENV_KEYS = [
|
|
|
13771
14279
|
"OPENSHIP_ACME_CA_BUNDLE",
|
|
13772
14280
|
"OPENSHIP_ACME_TOS_AGREED"
|
|
13773
14281
|
];
|
|
14282
|
+
function hostGatewayEnv(prev) {
|
|
14283
|
+
const value = process.env.OPENSHIP_HOST_GATEWAY?.trim() || prev.OPENSHIP_HOST_GATEWAY?.trim();
|
|
14284
|
+
return value ? [`OPENSHIP_HOST_GATEWAY=${value}`] : [];
|
|
14285
|
+
}
|
|
14286
|
+
function rootlessSocketCandidates() {
|
|
14287
|
+
const runtimeDir = process.env.XDG_RUNTIME_DIR?.trim();
|
|
14288
|
+
const sudoUid = process.env.SUDO_UID?.trim();
|
|
14289
|
+
let uid;
|
|
14290
|
+
try {
|
|
14291
|
+
uid = userInfo().uid;
|
|
14292
|
+
} catch {
|
|
14293
|
+
}
|
|
14294
|
+
return [
|
|
14295
|
+
...runtimeDir ? [join6(runtimeDir, "docker.sock")] : [],
|
|
14296
|
+
...sudoUid ? [`/run/user/${sudoUid}/docker.sock`] : [],
|
|
14297
|
+
...uid !== void 0 ? [`/run/user/${uid}/docker.sock`] : []
|
|
14298
|
+
];
|
|
14299
|
+
}
|
|
14300
|
+
function detectDockerSocketPath() {
|
|
14301
|
+
const fromClient = resolveLocalDockerSocketPath(void 0, process.env);
|
|
14302
|
+
if (fromClient !== DEFAULT_DOCKER_SOCKET_PATH) return fromClient;
|
|
14303
|
+
if (existsSync4(DEFAULT_DOCKER_SOCKET_PATH)) return DEFAULT_DOCKER_SOCKET_PATH;
|
|
14304
|
+
return rootlessSocketCandidates().find((p2) => existsSync4(p2)) ?? DEFAULT_DOCKER_SOCKET_PATH;
|
|
14305
|
+
}
|
|
14306
|
+
function resolveDockerSocket(prev) {
|
|
14307
|
+
const pinned = process.env.OPENSHIP_DOCKER_SOCKET?.trim() || prev.OPENSHIP_DOCKER_SOCKET?.trim();
|
|
14308
|
+
return pinned ? { path: pinned, pinned: true } : { path: detectDockerSocketPath(), pinned: false };
|
|
14309
|
+
}
|
|
14310
|
+
function dockerSocketEnv(prev) {
|
|
14311
|
+
const { path, pinned } = resolveDockerSocket(prev);
|
|
14312
|
+
return pinned || path !== DEFAULT_DOCKER_SOCKET_PATH ? [`OPENSHIP_DOCKER_SOCKET=${path}`] : [];
|
|
14313
|
+
}
|
|
14314
|
+
function warnMissingDockerSocket(path) {
|
|
14315
|
+
console.warn(
|
|
14316
|
+
`
|
|
14317
|
+
! The Docker socket this install mounts into the api isn't there:
|
|
14318
|
+
${path}
|
|
14319
|
+
Docker will create that path as an empty DIRECTORY, so the stack will come up
|
|
14320
|
+
and then fail every container operation with a transport error naming neither
|
|
14321
|
+
this path nor the cause (#482). Point it at the real socket:
|
|
14322
|
+
docker context inspect --format '{{.Endpoints.docker.Host}}'
|
|
14323
|
+
OPENSHIP_DOCKER_SOCKET=/run/user/$(id -u)/docker.sock openship up --compose
|
|
14324
|
+
(a rootless daemon keeps its socket under the daemon user's runtime dir).
|
|
14325
|
+
`
|
|
14326
|
+
);
|
|
14327
|
+
}
|
|
13774
14328
|
function renderAcmeEnv(prev) {
|
|
13775
14329
|
return ACME_ENV_KEYS.flatMap((key) => {
|
|
13776
14330
|
const value = process.env[key]?.trim() || prev[key]?.trim();
|
|
@@ -13800,7 +14354,19 @@ function resolveEnvConfig(prev, opts) {
|
|
|
13800
14354
|
registry: keepConfig(prev, "OPENSHIP_IMAGE_REGISTRY", opts.registry) ?? DEFAULT_IMAGE_REGISTRY,
|
|
13801
14355
|
// Tri-state: the flag is absent on a plain re-run, so fall back to what the
|
|
13802
14356
|
// install chose rather than silently re-granting host control.
|
|
13803
|
-
hostControl: opts.noHostControl === void 0 ? prev.OPENSHIP_HOST_CONTROL !== "false" : !opts.noHostControl
|
|
14357
|
+
hostControl: opts.noHostControl === void 0 ? prev.OPENSHIP_HOST_CONTROL !== "false" : !opts.noHostControl,
|
|
14358
|
+
// Sticky too: `--mail` is only the install-time default, so a plain re-run
|
|
14359
|
+
// (or `openship update`) must not hand a mail box back the platform shell.
|
|
14360
|
+
// Turning it OFF is the dashboard's job — Settings → Instance stores the mode
|
|
14361
|
+
// in the database, which wins over this env var either way.
|
|
14362
|
+
productMode: opts.mail || keepConfig(prev, "OPENSHIP_PRODUCT") === "mail" ? "mail" : "platform",
|
|
14363
|
+
// Sticky like every other operator choice here. These were literals in
|
|
14364
|
+
// `renderEnv`, so a box where `host.docker.internal:22` is simply not the
|
|
14365
|
+
// right address — rootless Docker, sshd on another port — had nowhere to say
|
|
14366
|
+
// so, and every `openship up` overwrote a hand-edited `.env` back to the
|
|
14367
|
+
// value that didn't work (#490/#482).
|
|
14368
|
+
hostSshHost: keepConfig(prev, "OPENSHIP_HOST_SSH_HOST", opts.hostSshHost) ?? HOST_CHANNEL_DEFAULT_HOST,
|
|
14369
|
+
hostSshPort: keepConfig(prev, "OPENSHIP_HOST_SSH_PORT", opts.hostSshPort) ?? String(HOST_CHANNEL_DEFAULT_PORT)
|
|
13804
14370
|
};
|
|
13805
14371
|
}
|
|
13806
14372
|
function toPort(value) {
|
|
@@ -13814,6 +14380,20 @@ function composeEnvPorts() {
|
|
|
13814
14380
|
function composeBindAddr() {
|
|
13815
14381
|
return readEnvFile().OPENSHIP_BIND_ADDR?.trim() || "127.0.0.1";
|
|
13816
14382
|
}
|
|
14383
|
+
function composeHostChannel() {
|
|
14384
|
+
const env = readEnvFile();
|
|
14385
|
+
const host = env.OPENSHIP_HOST_SSH_HOST?.trim();
|
|
14386
|
+
if (!host) return null;
|
|
14387
|
+
return {
|
|
14388
|
+
host,
|
|
14389
|
+
port: toPort(env.OPENSHIP_HOST_SSH_PORT) ?? 22,
|
|
14390
|
+
user: env.OPENSHIP_HOST_SSH_USER?.trim() || "root"
|
|
14391
|
+
};
|
|
14392
|
+
}
|
|
14393
|
+
function composeHostChannelExpected() {
|
|
14394
|
+
if (!existsSync4(COMPOSE_FILE)) return false;
|
|
14395
|
+
return readEnvFile().OPENSHIP_HOST_CONTROL !== "false";
|
|
14396
|
+
}
|
|
13817
14397
|
function composeTrustedOriginUrls() {
|
|
13818
14398
|
const env = readEnvFile();
|
|
13819
14399
|
return [env.OPENSHIP_PUBLIC_URL, env.OPENSHIP_EXTRA_TRUSTED_ORIGINS].filter(
|
|
@@ -13821,7 +14401,7 @@ function composeTrustedOriginUrls() {
|
|
|
13821
14401
|
);
|
|
13822
14402
|
}
|
|
13823
14403
|
function composeHeldPorts() {
|
|
13824
|
-
const r2 =
|
|
14404
|
+
const r2 = spawnSync4(
|
|
13825
14405
|
"docker",
|
|
13826
14406
|
["ps", "--format", '{{.Label "com.docker.compose.project.config_files"}} {{.Ports}}'],
|
|
13827
14407
|
{ encoding: "utf8" }
|
|
@@ -13847,8 +14427,24 @@ async function resolveComposePorts(prefs, opts = {}) {
|
|
|
13847
14427
|
opts
|
|
13848
14428
|
);
|
|
13849
14429
|
}
|
|
13850
|
-
|
|
13851
|
-
|
|
14430
|
+
var OMITTABLE_MANAGED_KEYS = /* @__PURE__ */ new Set([
|
|
14431
|
+
"OPENSHIP_HOST_SSH_HOST",
|
|
14432
|
+
"OPENSHIP_HOST_SSH_USER",
|
|
14433
|
+
"OPENSHIP_HOST_SSH_PORT",
|
|
14434
|
+
"OPENSHIP_HOST_SSH_KEY",
|
|
14435
|
+
"OPENSHIP_HOST_KEY_PATH",
|
|
14436
|
+
"OPENSHIP_HOST_GATEWAY",
|
|
14437
|
+
"TRUST_PROXY"
|
|
14438
|
+
]);
|
|
14439
|
+
var envKeyOf = (line) => line.match(/^([A-Z0-9_]+)=/)?.[1];
|
|
14440
|
+
function operatorEnvPassthrough(managed, prev) {
|
|
14441
|
+
return Object.entries(prev).filter(([key]) => !managed.has(key) && !OMITTABLE_MANAGED_KEYS.has(key)).map(([key, value]) => `${key}=${value}`);
|
|
14442
|
+
}
|
|
14443
|
+
var PRESERVED_ENV_HEADER = "# Preserved from your existing .env \u2014 set by you, not by `openship up` (#485).";
|
|
14444
|
+
function resolveImageVersion(opts) {
|
|
14445
|
+
return opts.version?.trim() || process.env.OPENSHIP_VERSION?.trim() || (true ? "0.6.5" : "latest");
|
|
14446
|
+
}
|
|
14447
|
+
function managedEnvLines(opts, host, cfg, prev) {
|
|
13852
14448
|
const lines = [
|
|
13853
14449
|
"# Managed by `openship up`. Secrets are generated once and preserved.",
|
|
13854
14450
|
// Do NOT edit on a live install — it is the volume prefix (see composeProjectName).
|
|
@@ -13859,8 +14455,17 @@ function renderEnv(opts, host, cfg) {
|
|
|
13859
14455
|
// Read by createHostExecutor (throws when false) and by the servers list
|
|
13860
14456
|
// (hides the local row). Written explicitly so the policy is visible in .env.
|
|
13861
14457
|
`OPENSHIP_HOST_CONTROL=${cfg.hostControl ? "true" : "false"}`,
|
|
14458
|
+
// The host side of the api's docker socket mount, when it isn't the rootful
|
|
14459
|
+
// default. Unlike the host channel above this is NOT optional plumbing — every
|
|
14460
|
+
// deploy goes through it — so it is resolved on every run, host control or not.
|
|
14461
|
+
...dockerSocketEnv(prev),
|
|
14462
|
+
// Which shell the dashboard defaults to. Emitted on EVERY run, including the
|
|
14463
|
+
// "platform" default: that keeps the key permanently in the managed set, so
|
|
14464
|
+
// the #485 passthrough can never mistake it for operator config (which would
|
|
14465
|
+
// pin it and make the Settings → Instance toggle look broken after an `up`).
|
|
14466
|
+
`OPENSHIP_PRODUCT=${cfg.productMode}`,
|
|
13862
14467
|
`OPENSHIP_IMAGE_REGISTRY=${cfg.registry}`,
|
|
13863
|
-
`OPENSHIP_VERSION=${opts
|
|
14468
|
+
`OPENSHIP_VERSION=${resolveImageVersion(opts)}`,
|
|
13864
14469
|
`POSTGRES_PASSWORD=${keepSecret(prev, "POSTGRES_PASSWORD")}`,
|
|
13865
14470
|
// Pinned once (see resolvePgData): fresh install → subdir, existing volume → root.
|
|
13866
14471
|
`OPENSHIP_PGDATA=${resolvePgData(prev)}`,
|
|
@@ -13885,19 +14490,33 @@ function renderEnv(opts, host, cfg) {
|
|
|
13885
14490
|
if (cfg.trustProxy) lines.push("TRUST_PROXY=true");
|
|
13886
14491
|
if (host) {
|
|
13887
14492
|
lines.push(
|
|
13888
|
-
|
|
14493
|
+
`OPENSHIP_HOST_SSH_HOST=${cfg.hostSshHost}`,
|
|
13889
14494
|
`OPENSHIP_HOST_SSH_USER=${host.user}`,
|
|
13890
|
-
|
|
14495
|
+
`OPENSHIP_HOST_SSH_PORT=${cfg.hostSshPort}`,
|
|
13891
14496
|
"OPENSHIP_HOST_SSH_KEY=/run/secrets/openship_host_key",
|
|
13892
|
-
`OPENSHIP_HOST_KEY_PATH=${host.keyPath}
|
|
14497
|
+
`OPENSHIP_HOST_KEY_PATH=${host.keyPath}`,
|
|
14498
|
+
...hostGatewayEnv(prev)
|
|
13893
14499
|
);
|
|
13894
14500
|
}
|
|
13895
|
-
return lines
|
|
14501
|
+
return lines;
|
|
14502
|
+
}
|
|
14503
|
+
function renderEnvAndCarried(opts, host, cfg, prev) {
|
|
14504
|
+
const lines = managedEnvLines(opts, host, cfg, prev);
|
|
14505
|
+
const managed = new Set(lines.map(envKeyOf).filter((k2) => !!k2));
|
|
14506
|
+
const preserved = operatorEnvPassthrough(managed, prev);
|
|
14507
|
+
const body = preserved.length ? [...lines, "", PRESERVED_ENV_HEADER, ...preserved] : lines;
|
|
14508
|
+
return {
|
|
14509
|
+
text: body.join("\n") + "\n",
|
|
14510
|
+
carried: preserved.map((line) => line.slice(0, line.indexOf("=")))
|
|
14511
|
+
};
|
|
14512
|
+
}
|
|
14513
|
+
function renderEnv(opts, host, cfg) {
|
|
14514
|
+
return renderEnvAndCarried(opts, host, cfg, readEnvFile()).text;
|
|
13896
14515
|
}
|
|
13897
14516
|
function sourceBuildDir() {
|
|
13898
14517
|
const marker = readSourceInstall();
|
|
13899
14518
|
if (!marker?.dir) return null;
|
|
13900
|
-
const hasAll = BUILT_SERVICES.every((s2) => existsSync4(
|
|
14519
|
+
const hasAll = BUILT_SERVICES.every((s2) => existsSync4(join6(marker.dir, s2.dockerfile)));
|
|
13901
14520
|
return hasAll ? marker.dir : null;
|
|
13902
14521
|
}
|
|
13903
14522
|
function renderBuildOverride(repoDir) {
|
|
@@ -13916,19 +14535,48 @@ function materialize(opts) {
|
|
|
13916
14535
|
mkdirSync3(COMPOSE_DIR, { recursive: true, mode: 448 });
|
|
13917
14536
|
const prev = readEnvFile();
|
|
13918
14537
|
const cfg = resolveEnvConfig(prev, opts);
|
|
13919
|
-
const
|
|
14538
|
+
const provision = provisionHostSshChannel(cfg);
|
|
14539
|
+
let host = provision.channel;
|
|
14540
|
+
let kept = false;
|
|
14541
|
+
if (!host && provision.issue) {
|
|
14542
|
+
const previousChannel = carriedHostChannel(prev);
|
|
14543
|
+
if (previousChannel) {
|
|
14544
|
+
host = previousChannel;
|
|
14545
|
+
kept = true;
|
|
14546
|
+
}
|
|
14547
|
+
}
|
|
14548
|
+
if (!cfg.hostControl) retireHostSshChannel(prev);
|
|
14549
|
+
if (provision.issue && !opts.alreadyFetched) {
|
|
14550
|
+
console.warn(
|
|
14551
|
+
renderHostChannelIssue(provision.issue, {
|
|
14552
|
+
target: `${cfg.hostSshHost}:${cfg.hostSshPort}`,
|
|
14553
|
+
kept
|
|
14554
|
+
})
|
|
14555
|
+
);
|
|
14556
|
+
}
|
|
14557
|
+
const socket = resolveDockerSocket(prev);
|
|
14558
|
+
if (!opts.alreadyFetched && !existsSync4(socket.path)) warnMissingDockerSocket(socket.path);
|
|
13920
14559
|
let before = "";
|
|
13921
14560
|
try {
|
|
13922
14561
|
before = readFileSync3(ENV_FILE, "utf8");
|
|
13923
14562
|
} catch {
|
|
13924
14563
|
}
|
|
13925
|
-
const rendered =
|
|
14564
|
+
const { text: rendered, carried } = renderEnvAndCarried(opts, host, cfg, prev);
|
|
13926
14565
|
writeFileSync2(COMPOSE_FILE, COMPOSE_YAML);
|
|
13927
|
-
|
|
14566
|
+
writeEnvFile(rendered, before);
|
|
14567
|
+
if (carried.length) {
|
|
14568
|
+
console.log(` Kept ${carried.length} operator-set .env variable(s): ${carried.join(", ")}`);
|
|
14569
|
+
}
|
|
13928
14570
|
const buildDir = opts.build === false ? null : sourceBuildDir();
|
|
13929
14571
|
if (buildDir) writeFileSync2(BUILD_FILE, renderBuildOverride(buildDir));
|
|
13930
14572
|
return { buildDir, cfg, envChanged: before !== "" && rendered !== before };
|
|
13931
14573
|
}
|
|
14574
|
+
function writeEnvFile(rendered, before) {
|
|
14575
|
+
if (rendered === before) return;
|
|
14576
|
+
if (before) writeFileSync2(ENV_BACKUP_FILE, before, { mode: 384 });
|
|
14577
|
+
writeFileSync2(ENV_TMP_FILE, rendered, { mode: 384 });
|
|
14578
|
+
renameSync(ENV_TMP_FILE, ENV_FILE);
|
|
14579
|
+
}
|
|
13932
14580
|
var SECRET_ENV_KEY = /(PASSWORD|SECRET|TOKEN|HMAC_KEY)$/;
|
|
13933
14581
|
function composePlan(opts) {
|
|
13934
14582
|
const prev = readEnvFile();
|
|
@@ -13941,13 +14589,14 @@ function composePlan(opts) {
|
|
|
13941
14589
|
const at2 = line.indexOf("=");
|
|
13942
14590
|
if (at2 < 1 || line.startsWith("#")) continue;
|
|
13943
14591
|
const key = line.slice(0, at2);
|
|
14592
|
+
const value = line.slice(at2 + 1);
|
|
13944
14593
|
if (!SECRET_ENV_KEY.test(key)) {
|
|
13945
|
-
settings.push({ key, value
|
|
14594
|
+
settings.push({ key, value });
|
|
13946
14595
|
continue;
|
|
13947
14596
|
}
|
|
13948
|
-
const
|
|
13949
|
-
if (
|
|
13950
|
-
settings.push({ key, value:
|
|
14597
|
+
const preserved = !!prev[key] && prev[key] === value;
|
|
14598
|
+
if (!preserved) newSecrets.push(key);
|
|
14599
|
+
settings.push({ key, value: preserved ? "<preserved>" : "<generated on this run>" });
|
|
13951
14600
|
}
|
|
13952
14601
|
return {
|
|
13953
14602
|
composeFile: COMPOSE_FILE,
|
|
@@ -13957,6 +14606,8 @@ function composePlan(opts) {
|
|
|
13957
14606
|
yaml: COMPOSE_YAML,
|
|
13958
14607
|
settings,
|
|
13959
14608
|
newSecrets,
|
|
14609
|
+
secretRotation: opts.resetSecrets ? null : secretRotationRisk(prev),
|
|
14610
|
+
pgDataRisk: pgDataDetectionRisk(prev),
|
|
13960
14611
|
existing: Object.keys(prev).length > 0,
|
|
13961
14612
|
mountDirs: EDGE_CONTAINER_MOUNTS.map((m2) => m2.host),
|
|
13962
14613
|
buildDir,
|
|
@@ -13964,7 +14615,7 @@ function composePlan(opts) {
|
|
|
13964
14615
|
};
|
|
13965
14616
|
}
|
|
13966
14617
|
function dbVolumeExists(project) {
|
|
13967
|
-
const r2 =
|
|
14618
|
+
const r2 = spawnSync4("docker", ["volume", "inspect", `${project}_postgres_data`], {
|
|
13968
14619
|
stdio: "ignore"
|
|
13969
14620
|
});
|
|
13970
14621
|
return r2.status === 0;
|
|
@@ -13972,18 +14623,69 @@ function dbVolumeExists(project) {
|
|
|
13972
14623
|
function compose(args, opts) {
|
|
13973
14624
|
const files = ["-f", COMPOSE_FILE];
|
|
13974
14625
|
if (opts?.withBuildOverride) files.push("-f", BUILD_FILE);
|
|
13975
|
-
const r2 =
|
|
14626
|
+
const r2 = spawnSync4("docker", ["compose", ...files, ...args], {
|
|
13976
14627
|
cwd: COMPOSE_DIR,
|
|
13977
14628
|
stdio: opts?.quiet ? "ignore" : "inherit"
|
|
13978
14629
|
});
|
|
13979
14630
|
return r2.status ?? 1;
|
|
13980
14631
|
}
|
|
14632
|
+
function missingPinnedImages(registry, version) {
|
|
14633
|
+
const missing = [];
|
|
14634
|
+
for (const { service } of BUILT_SERVICES) {
|
|
14635
|
+
const ref = `${registry}/openship-${service}:${version}`;
|
|
14636
|
+
const r2 = spawnSync4("docker", ["manifest", "inspect", ref], { encoding: "utf8" });
|
|
14637
|
+
if (r2.status === 0) continue;
|
|
14638
|
+
const stderr = `${r2.stderr ?? ""}`.toLowerCase();
|
|
14639
|
+
if (/manifest unknown|no such manifest|manifest for .+ not found/.test(stderr)) {
|
|
14640
|
+
missing.push(ref);
|
|
14641
|
+
continue;
|
|
14642
|
+
}
|
|
14643
|
+
return null;
|
|
14644
|
+
}
|
|
14645
|
+
return missing;
|
|
14646
|
+
}
|
|
14647
|
+
function pinnedImagesReady(opts) {
|
|
14648
|
+
if (sourceBuildDir()) return true;
|
|
14649
|
+
const prev = readEnvFile();
|
|
14650
|
+
const version = resolveImageVersion(opts);
|
|
14651
|
+
const missing = missingPinnedImages(resolveEnvConfig(prev, opts).registry, version);
|
|
14652
|
+
if (!missing || missing.length === 0) return true;
|
|
14653
|
+
const prevVersion = prev.OPENSHIP_VERSION?.trim();
|
|
14654
|
+
const out = [
|
|
14655
|
+
"",
|
|
14656
|
+
` The pinned image tag \`${version}\` isn't in the registry yet:`,
|
|
14657
|
+
...missing.map((ref) => ` ${ref}`),
|
|
14658
|
+
"",
|
|
14659
|
+
" The CLI reached npm before its container images finished publishing \u2014 a",
|
|
14660
|
+
" release-ordering race (#486). Nothing on this box was changed.",
|
|
14661
|
+
"",
|
|
14662
|
+
" Any one of these gets you moving:",
|
|
14663
|
+
" \u2022 Wait a few minutes and re-run \u2014 the images usually land right after a release.",
|
|
14664
|
+
" \u2022 Pin a tag that exists:",
|
|
14665
|
+
" openship up --compose --image-version <tag>",
|
|
14666
|
+
" OPENSHIP_VERSION=<tag> openship up --compose"
|
|
14667
|
+
];
|
|
14668
|
+
if (prevVersion && prevVersion !== version) {
|
|
14669
|
+
out.push(` \u2022 Your current install runs \`${prevVersion}\`, a tag known good on this box.`);
|
|
14670
|
+
}
|
|
14671
|
+
out.push("");
|
|
14672
|
+
console.error(out.join("\n"));
|
|
14673
|
+
return false;
|
|
14674
|
+
}
|
|
13981
14675
|
function composePrefetch(opts) {
|
|
13982
|
-
const
|
|
14676
|
+
const rotation = composeSecretRotationRisk(opts);
|
|
14677
|
+
if (rotation) {
|
|
14678
|
+
console.error(renderSecretRotationRefusal(rotation));
|
|
14679
|
+
return { ok: false, envChanged: false };
|
|
14680
|
+
}
|
|
14681
|
+
const { buildDir, envChanged } = materialize(opts);
|
|
13983
14682
|
if (buildDir) {
|
|
13984
|
-
return
|
|
14683
|
+
return {
|
|
14684
|
+
ok: compose(["pull", "postgres", "redis"], { withBuildOverride: true }) === 0 && compose(["build"], { withBuildOverride: true }) === 0,
|
|
14685
|
+
envChanged
|
|
14686
|
+
};
|
|
13985
14687
|
}
|
|
13986
|
-
return compose(["pull"]) === 0;
|
|
14688
|
+
return { ok: compose(["pull"]) === 0, envChanged };
|
|
13987
14689
|
}
|
|
13988
14690
|
function ensureEdgeMountDirs() {
|
|
13989
14691
|
const failed = [];
|
|
@@ -13997,15 +14699,32 @@ function ensureEdgeMountDirs() {
|
|
|
13997
14699
|
return failed;
|
|
13998
14700
|
}
|
|
13999
14701
|
function isRootlessDocker() {
|
|
14000
|
-
const r2 =
|
|
14702
|
+
const r2 = spawnSync4("docker", ["info", "-f", "{{println .SecurityOptions}}"], {
|
|
14001
14703
|
encoding: "utf8"
|
|
14002
14704
|
});
|
|
14003
14705
|
return r2.status === 0 && /rootless/i.test(r2.stdout ?? "");
|
|
14004
14706
|
}
|
|
14005
14707
|
async function composeUp(opts) {
|
|
14006
|
-
|
|
14708
|
+
if (!opts.alreadyFetched && !pinnedImagesReady(opts)) {
|
|
14709
|
+
const pre = resolveEnvConfig(readEnvFile(), opts);
|
|
14710
|
+
return { ok: false, apiPort: pre.apiPort, dashPort: pre.dashPort };
|
|
14711
|
+
}
|
|
14712
|
+
const rotation = composeSecretRotationRisk(opts);
|
|
14713
|
+
if (rotation) {
|
|
14714
|
+
const pre = resolveEnvConfig(readEnvFile(), opts);
|
|
14715
|
+
console.error(renderSecretRotationRefusal(rotation));
|
|
14716
|
+
return { ok: false, refused: true, apiPort: pre.apiPort, dashPort: pre.dashPort };
|
|
14717
|
+
}
|
|
14718
|
+
const pgData = composePgDataRisk(opts);
|
|
14719
|
+
if (pgData) {
|
|
14720
|
+
const pre = resolveEnvConfig(readEnvFile(), opts);
|
|
14721
|
+
console.error(renderPgDataRefusal(pgData));
|
|
14722
|
+
return { ok: false, refused: true, apiPort: pre.apiPort, dashPort: pre.dashPort };
|
|
14723
|
+
}
|
|
14724
|
+
const { buildDir, cfg, envChanged: rendered } = materialize(opts);
|
|
14007
14725
|
const apiPort = cfg.apiPort;
|
|
14008
14726
|
const dashPort = cfg.dashPort;
|
|
14727
|
+
const envChanged = opts.envChanged ?? rendered;
|
|
14009
14728
|
const up = (extra = {}) => compose(
|
|
14010
14729
|
envChanged ? ["up", "-d", "--force-recreate", ...ENV_CONSUMING_SERVICES] : ["up", "-d"],
|
|
14011
14730
|
extra
|
|
@@ -14013,6 +14732,7 @@ async function composeUp(opts) {
|
|
|
14013
14732
|
const env = readEnvFile();
|
|
14014
14733
|
const project = env.COMPOSE_PROJECT_NAME || "openship";
|
|
14015
14734
|
removeOrphanedStack(project);
|
|
14735
|
+
reconcileEdgeContainerName(project);
|
|
14016
14736
|
migrateLegacyEdgeVolumes(project);
|
|
14017
14737
|
warnOrphanedVolumes(project);
|
|
14018
14738
|
const unmakeableMounts = ensureEdgeMountDirs();
|
|
@@ -14079,7 +14799,7 @@ function composeUninstall(opts = {}) {
|
|
|
14079
14799
|
const version = env.OPENSHIP_VERSION || "latest";
|
|
14080
14800
|
for (const { service } of BUILT_SERVICES) {
|
|
14081
14801
|
const ref = `${registry}/openship-${service}:${version}`;
|
|
14082
|
-
if (
|
|
14802
|
+
if (spawnSync4("docker", ["image", "rm", "-f", ref], { stdio: "ignore" }).status === 0) {
|
|
14083
14803
|
removedImages.push(ref);
|
|
14084
14804
|
}
|
|
14085
14805
|
}
|
|
@@ -14087,14 +14807,15 @@ function composeUninstall(opts = {}) {
|
|
|
14087
14807
|
return { ok, removedImages };
|
|
14088
14808
|
}
|
|
14089
14809
|
async function composeUpdate(version) {
|
|
14090
|
-
if (!existsSync4(COMPOSE_FILE)) return false;
|
|
14091
|
-
|
|
14810
|
+
if (!existsSync4(COMPOSE_FILE)) return { applied: false, refused: false };
|
|
14811
|
+
const r2 = await composeUp(version ? { version } : {});
|
|
14812
|
+
return { applied: r2.ok, refused: !!r2.refused };
|
|
14092
14813
|
}
|
|
14093
14814
|
function composePs() {
|
|
14094
14815
|
return compose(["ps"]);
|
|
14095
14816
|
}
|
|
14096
14817
|
function composeRunning() {
|
|
14097
|
-
const r2 =
|
|
14818
|
+
const r2 = spawnSync4(
|
|
14098
14819
|
"docker",
|
|
14099
14820
|
["compose", "-f", COMPOSE_FILE, "ps", "--status", "running", "-q"],
|
|
14100
14821
|
{ cwd: COMPOSE_DIR, encoding: "utf8" }
|
|
@@ -14102,6 +14823,7 @@ function composeRunning() {
|
|
|
14102
14823
|
return r2.status === 0 && r2.stdout.trim().length > 0;
|
|
14103
14824
|
}
|
|
14104
14825
|
function composeStart() {
|
|
14826
|
+
reconcileEdgeContainerName(readEnvFile().COMPOSE_PROJECT_NAME || "openship");
|
|
14105
14827
|
return compose(["up", "-d"], { quiet: true }) === 0;
|
|
14106
14828
|
}
|
|
14107
14829
|
function composeRestart() {
|
|
@@ -14110,112 +14832,14 @@ function composeRestart() {
|
|
|
14110
14832
|
function composeInternalToken() {
|
|
14111
14833
|
return readEnvFile().INTERNAL_TOKEN ?? null;
|
|
14112
14834
|
}
|
|
14113
|
-
|
|
14114
|
-
|
|
14115
|
-
|
|
14116
|
-
|
|
14117
|
-
|
|
14118
|
-
|
|
14119
|
-
if (r2.ok) return true;
|
|
14120
|
-
} catch {
|
|
14121
|
-
}
|
|
14122
|
-
await new Promise((r2) => setTimeout(r2, 1e3));
|
|
14123
|
-
}
|
|
14124
|
-
return false;
|
|
14125
|
-
}
|
|
14126
|
-
async function waitForEdgeRunning(tries) {
|
|
14127
|
-
const exec = new LocalExecutor();
|
|
14128
|
-
for (let i2 = 0; i2 < tries; i2++) {
|
|
14129
|
-
const out = await exec.exec(`docker inspect -f '{{.State.Running}}' ${EDGE_CONTAINER_NAME}`).catch(() => "");
|
|
14130
|
-
if (out.trim() === "true") return true;
|
|
14131
|
-
await new Promise((r2) => setTimeout(r2, 1e3));
|
|
14132
|
-
}
|
|
14133
|
-
return false;
|
|
14134
|
-
}
|
|
14135
|
-
async function importMigratedSites(apiPort, sites, certPems, staticRootOverrides) {
|
|
14136
|
-
const token = composeInternalToken();
|
|
14137
|
-
if (!token) {
|
|
14138
|
-
const error = "couldn't read the stack's internal token";
|
|
14139
|
-
console.log(chalk.yellow(` Skipping site import \u2014 ${error}. Re-run to retry.
|
|
14140
|
-
`));
|
|
14141
|
-
return { ok: false, registered: [], error };
|
|
14142
|
-
}
|
|
14143
|
-
const spinner = ora(
|
|
14144
|
-
`Importing ${sites.length} migrated site${sites.length === 1 ? "" : "s"} into the edge\u2026`
|
|
14145
|
-
).start();
|
|
14146
|
-
if (!await waitForApiHealth(apiPort, 60)) {
|
|
14147
|
-
const error = "API didn't become healthy in time";
|
|
14148
|
-
spinner.warn(`${error} \u2014 migrated sites not imported. Re-run \`openship up\` to retry.`);
|
|
14149
|
-
return { ok: false, registered: [], error };
|
|
14150
|
-
}
|
|
14151
|
-
if (!await waitForEdgeRunning(60)) {
|
|
14152
|
-
const error = `the ${EDGE_CONTAINER_NAME} container isn't running yet`;
|
|
14153
|
-
spinner.warn(`${error} \u2014 migrated sites not imported. Re-run \`openship up\` to retry.`);
|
|
14154
|
-
return { ok: false, registered: [], error };
|
|
14155
|
-
}
|
|
14156
|
-
try {
|
|
14157
|
-
const r2 = await fetch(`http://127.0.0.1:${apiPort}/api/system/edge/import-sites`, {
|
|
14158
|
-
method: "POST",
|
|
14159
|
-
headers: { "Content-Type": "application/json", "X-Internal-Token": token },
|
|
14160
|
-
body: JSON.stringify({ sites, certPems, staticRootOverrides }),
|
|
14161
|
-
signal: AbortSignal.timeout(12e4)
|
|
14162
|
-
});
|
|
14163
|
-
const data = await r2.json().catch(() => ({}));
|
|
14164
|
-
if (!r2.ok) {
|
|
14165
|
-
const error = data.error ?? `HTTP ${r2.status}`;
|
|
14166
|
-
spinner.warn(`Site import failed: ${error}. Re-run to retry.`);
|
|
14167
|
-
return { ok: false, registered: [], error };
|
|
14168
|
-
}
|
|
14169
|
-
const registered = data.registered ?? [];
|
|
14170
|
-
const warnings = data.warnings ?? [];
|
|
14171
|
-
const missed = sites.length - registered.length;
|
|
14172
|
-
if (sites.length > 0 && registered.length === 0) {
|
|
14173
|
-
spinner.fail(
|
|
14174
|
-
`Migrated NONE of the ${sites.length} site${sites.length === 1 ? "" : "s"} \u2014 Openship holds :80/:443 and ${sites.length === 1 ? "that hostname is" : "those hostnames are"} NOT being served.`
|
|
14175
|
-
);
|
|
14176
|
-
for (const w2 of warnings.slice(0, 8)) console.log(chalk.red(` \u2022 ${w2}`));
|
|
14177
|
-
const edgeLog = await edgeCrashReason(new LocalExecutor());
|
|
14178
|
-
if (edgeLog) {
|
|
14179
|
-
console.log(chalk.red(`
|
|
14180
|
-
The edge container is not running. Its log says:`));
|
|
14181
|
-
console.log(chalk.red(` ${edgeLog}`));
|
|
14182
|
-
}
|
|
14183
|
-
console.log(
|
|
14184
|
-
chalk.yellow(
|
|
14185
|
-
`
|
|
14186
|
-
Retry the import with \`openship up\` once the cause above is fixed, or put your
|
|
14187
|
-
previous proxy back: docker stop ${EDGE_CONTAINER_NAME} && sudo systemctl enable --now nginx
|
|
14188
|
-
`
|
|
14189
|
-
)
|
|
14190
|
-
);
|
|
14191
|
-
return { ok: false, registered, error: warnings[0] ?? "no sites were registered" };
|
|
14192
|
-
}
|
|
14193
|
-
if (missed > 0) {
|
|
14194
|
-
spinner.warn(
|
|
14195
|
-
`Migrated ${registered.length}/${sites.length} sites \u2014 ${missed} not served (see below).`
|
|
14196
|
-
);
|
|
14197
|
-
for (const w2 of warnings.slice(0, 8)) console.log(chalk.yellow(` \u2022 ${w2}`));
|
|
14198
|
-
return { ok: false, registered, error: `${missed} site(s) not registered` };
|
|
14199
|
-
}
|
|
14200
|
-
spinner.succeed(
|
|
14201
|
-
`Migrated ${registered.length} site${registered.length === 1 ? "" : "s"} into Openship's edge.`
|
|
14202
|
-
);
|
|
14203
|
-
for (const w2 of warnings.slice(0, 8)) console.log(chalk.dim(` \u2022 ${w2}`));
|
|
14204
|
-
return { ok: true, registered };
|
|
14205
|
-
} catch (e2) {
|
|
14206
|
-
const error = e2.message;
|
|
14207
|
-
spinner.warn(`Site import failed: ${error}. Re-run to retry.`);
|
|
14208
|
-
return { ok: false, registered: [], error };
|
|
14209
|
-
}
|
|
14210
|
-
}
|
|
14835
|
+
var composePaths = {
|
|
14836
|
+
dir: COMPOSE_DIR,
|
|
14837
|
+
file: COMPOSE_FILE,
|
|
14838
|
+
env: ENV_FILE,
|
|
14839
|
+
envBackup: ENV_BACKUP_FILE
|
|
14840
|
+
};
|
|
14211
14841
|
|
|
14212
14842
|
export {
|
|
14213
|
-
OS_DIR,
|
|
14214
|
-
IS_ALT_HOME,
|
|
14215
|
-
DATA_DIR,
|
|
14216
|
-
LOG_DIR,
|
|
14217
|
-
AUTH_SECRET_FILE,
|
|
14218
|
-
INTERNAL_TOKEN_FILE,
|
|
14219
14843
|
PORTS_FILE,
|
|
14220
14844
|
saveInstanceUrl,
|
|
14221
14845
|
readInstanceUrl,
|
|
@@ -14224,6 +14848,8 @@ export {
|
|
|
14224
14848
|
storedDashboardPort,
|
|
14225
14849
|
portMoveNotice,
|
|
14226
14850
|
resolvePorts,
|
|
14851
|
+
thisHost,
|
|
14852
|
+
startUnitHint,
|
|
14227
14853
|
DEFAULT_REPO,
|
|
14228
14854
|
SOURCE_CHECKOUT_DIR,
|
|
14229
14855
|
SOURCE_DIST_DIR,
|
|
@@ -14234,14 +14860,20 @@ export {
|
|
|
14234
14860
|
remoteSha,
|
|
14235
14861
|
rebuildFromSource,
|
|
14236
14862
|
readInstallMethod,
|
|
14237
|
-
dockerGap,
|
|
14238
14863
|
hasDockerCompose,
|
|
14239
14864
|
dockerInstallPreview,
|
|
14240
14865
|
ensureDocker,
|
|
14866
|
+
composeSecretRotationRisk,
|
|
14867
|
+
renderSecretRotationRefusal,
|
|
14868
|
+
composePgDataRisk,
|
|
14869
|
+
renderPgDataRefusal,
|
|
14870
|
+
composeHostChannel,
|
|
14871
|
+
composeHostChannelExpected,
|
|
14241
14872
|
composeTrustedOriginUrls,
|
|
14242
14873
|
resolveComposePorts,
|
|
14243
14874
|
sourceBuildDir,
|
|
14244
14875
|
composePlan,
|
|
14876
|
+
pinnedImagesReady,
|
|
14245
14877
|
composePrefetch,
|
|
14246
14878
|
composeUp,
|
|
14247
14879
|
composeDown,
|
|
@@ -14252,7 +14884,5 @@ export {
|
|
|
14252
14884
|
composeStart,
|
|
14253
14885
|
composeRestart,
|
|
14254
14886
|
composeInternalToken,
|
|
14255
|
-
|
|
14256
|
-
waitForEdgeRunning,
|
|
14257
|
-
importMigratedSites
|
|
14887
|
+
composePaths
|
|
14258
14888
|
};
|