openship 0.4.8 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -1
- package/dist/bare-FSUAM5V5.js +23 -0
- package/dist/{bowser-O3AEA6AN.js → bowser-HNOTPGJO.js} +2 -2
- package/dist/{chunk-5MZ624VX.js → chunk-2GWYYHOR.js} +1 -1
- package/dist/chunk-3FYIXVX5.js +8051 -0
- package/dist/{chunk-ILFETZOM.js → chunk-3LDWWY7U.js} +21 -1
- package/dist/{chunk-W73X43R7.js → chunk-3NQQXMDR.js} +3 -3
- package/dist/{chunk-OWED2GRM.js → chunk-42WQABXM.js} +2 -2
- package/dist/{chunk-TYVME3XD.js → chunk-6FH4AXVY.js} +20 -3
- package/dist/{chunk-OXWDY22G.js → chunk-6ODUBKUB.js} +3 -3
- package/dist/{chunk-DYPH4HCI.js → chunk-AO252K7Z.js} +1 -1
- package/dist/{chunk-XPPZ7B2J.js → chunk-B3CE2OMR.js} +14 -12
- package/dist/{chunk-IXCHURVK.js → chunk-DOD2BXHQ.js} +4 -4
- package/dist/{chunk-VWPEKOH6.js → chunk-DRK3MEDS.js} +3 -3
- package/dist/{chunk-V6AN6XLZ.js → chunk-EMXI4UCU.js} +125 -47
- package/dist/{chunk-GKHUSZCD.js → chunk-EP2TNALB.js} +2 -2
- package/dist/{chunk-SLTYVHU7.js → chunk-GUQ3QCJ7.js} +942 -519
- package/dist/chunk-I3DPMTSF.js +29 -0
- package/dist/{chunk-L2VOGB2X.js → chunk-IW2UKBVF.js} +3 -3
- package/dist/{chunk-VDMQJHPI.js → chunk-IXM5245F.js} +228 -13
- package/dist/{chunk-MZ2WEHHY.js → chunk-JIMUYEKN.js} +4 -4
- package/dist/chunk-OGY63DGD.js +705 -0
- package/dist/{chunk-O6ZZF52V.js → chunk-ONKEYSMY.js} +2 -2
- package/dist/{chunk-6APULWSU.js → chunk-OTLSJYAC.js} +1744 -118
- package/dist/{chunk-EI575GHN.js → chunk-PDW4L3NV.js} +3 -3
- package/dist/{chunk-5NYUCONN.js → chunk-PSZSKEPA.js} +33 -51
- package/dist/{chunk-KI2EY3WB.js → chunk-PXKCZB2I.js} +1 -1
- package/dist/{chunk-FXTBHTWY.js → chunk-QEH4QUBX.js} +649 -85
- package/dist/{chunk-6APIPIJY.js → chunk-QWHBRY2K.js} +5 -5
- package/dist/{chunk-6B5YCFFV.js → chunk-RVER2EOO.js} +5 -5
- package/dist/{chunk-Q2CIRAOK.js → chunk-RXQBD4ZY.js} +1 -1
- package/dist/{chunk-TGIINPVM.js → chunk-TWJCLD4W.js} +60 -10
- package/dist/{chunk-4Z5QO7N6.js → chunk-VDDMADOF.js} +3 -3
- package/dist/{chunk-BBHX7T4R.js → chunk-VE2YOK4I.js} +991 -254
- package/dist/{chunk-2D3I5G6P.js → chunk-VZ2ZHPS2.js} +73 -43
- package/dist/{chunk-AVIB7CT7.js → chunk-WNWQBB7F.js} +2 -2
- package/dist/{chunk-FRNAR33O.js → chunk-XZYFZ6A5.js} +3 -3
- package/dist/{chunk-RPHRPFEH.js → chunk-ZCFVHA25.js} +1 -1
- package/dist/{chunk-FPRHYBY2.js → chunk-ZFJXPCQP.js} +1 -1
- package/dist/{chunk-JYGD5WSN.js → chunk-ZWLDLTJP.js} +1 -1
- package/dist/cloud-E5M6VMCG.js +31 -0
- package/dist/{cloud-7QYOGF7F.js → cloud-KRTZ2IL5.js} +3 -3
- package/dist/{detect-AZLOHUFX.js → detect-ALF4GXRX.js} +7 -8
- package/dist/{dist-IQJ6H6UZ.js → dist-BM6NPWIH.js} +3 -3
- package/dist/{dist-es-X3B5QYKB.js → dist-es-2YRDXMUU.js} +6 -6
- package/dist/{dist-es-OSNJBLXB.js → dist-es-CCVZEFRZ.js} +5 -5
- package/dist/{dist-es-VNKHQVIB.js → dist-es-IOH6G4GM.js} +4 -4
- package/dist/{dist-es-RYHIPOTN.js → dist-es-JGBPABJI.js} +14 -14
- package/dist/{dist-es-JSTDUS7Q.js → dist-es-KWSMDPIP.js} +6 -6
- package/dist/{dist-es-5DY7K32L.js → dist-es-UYDDOVZ3.js} +6 -6
- package/dist/{dist-es-WV4AMOII.js → dist-es-XUOR4NQN.js} +8 -8
- package/dist/docker-QECQJ5P4.js +32 -0
- package/dist/docker-edge-executor-62N64MGS.js +17 -0
- package/dist/edge-import-CQWHN2Q3.js +48 -0
- package/dist/ensure-container-edge-MJVXJQG5.js +32 -0
- package/dist/{event-streams-EHRAKL75.js → event-streams-TR4GAC53.js} +4 -4
- package/dist/{executor-XJ6RP4Q2.js → executor-JELARVKC.js} +8 -8
- package/dist/index.js +1209 -560
- package/dist/{loadSso-46FQRIZS.js → loadSso-UGF4JMRK.js} +10 -10
- package/dist/{local-executor-V7U5VOZW.js → local-executor-WOFZD72X.js} +4 -4
- package/dist/nginx-AXGNWRKZ.js +31 -0
- package/dist/node-entry.js +4 -0
- package/dist/{noop-FMMFG2EZ.js → noop-NJ72F46T.js} +3 -3
- package/dist/openresty-lua-IE4P5YQX.js +81 -0
- package/dist/server/index.js +116248 -94749
- package/dist/server/lua/geo_country.lua +31 -6
- package/dist/server/lua/maxminddb.lua +403 -0
- package/dist/server/lua/mgmt_api.lua +431 -68
- package/dist/server/lua/pipe_log.lua +10 -4
- package/dist/server/lua/site_logger.lua +220 -12
- package/dist/server/migrations/0073_project_volumes_object_storage.sql +3 -0
- package/dist/server/migrations/0074_project_compose_path.sql +1 -0
- package/dist/server/migrations/0075_service_command_argv.sql +1 -0
- package/dist/server/migrations/0076_rollback_retention.sql +17 -0
- package/dist/server/migrations/0077_domain_redirect.sql +13 -0
- package/dist/server/migrations/0078_grant_source_scope.sql +30 -0
- package/dist/server/migrations/0079_project_readiness.sql +18 -0
- package/dist/server/migrations/0080_deployment_error_classification.sql +26 -0
- package/dist/server/migrations/0081_project_readiness_repair.sql +34 -0
- package/dist/server/migrations/0082_edge_target_verification.sql +39 -0
- package/dist/server/migrations/0083_service_incident.sql +84 -0
- package/dist/server/migrations/0084_analytics_daily_rollup.sql +72 -0
- package/dist/server/migrations/0085_resource_usage.sql +68 -0
- package/dist/server/migrations/0086_remote_infra_updates.sql +41 -0
- package/dist/server/migrations/0087_server_container_version_columns.sql +17 -0
- package/dist/server/migrations/0088_audit_source_and_settings.sql +27 -0
- package/dist/server/migrations/0089_auto_scan_infra.sql +7 -0
- package/dist/server/migrations/0090_project_collect_paths.sql +18 -0
- package/dist/server/migrations/0091_project_server_id.sql +26 -0
- package/dist/server/migrations/0092_project_internal_alias.sql +13 -0
- package/dist/server/migrations/0093_service_incident_org_index.sql +13 -0
- package/dist/server/migrations/0094_backup_restore_meta.sql +15 -0
- package/dist/server/migrations/0095_backup_restore_cancel.sql +19 -0
- package/dist/server/migrations/0096_backup_policy_retention_defaults.sql +20 -0
- package/dist/server/migrations/0097_update_status_upstream_only.sql +35 -0
- package/dist/server/migrations/meta/_journal.json +175 -0
- package/dist/setup-FA4PW4NU.js +21 -0
- package/dist/{signin-ITOPKKUB.js → signin-3M3UPB6K.js} +10 -10
- package/dist/{sso-oidc-NON2PD7Y.js → sso-oidc-3VDIYOEJ.js} +10 -10
- package/dist/{sts-HHFFSAY2.js → sts-B3WIYLXR.js} +9 -9
- package/package.json +6 -3
- package/dist/bare-JY5Y5ELU.js +0 -23
- package/dist/chunk-SJIFLLX4.js +0 -2931
- package/dist/chunk-VFINNTUL.js +0 -362
- package/dist/cloud-KYWY3RP4.js +0 -30
- package/dist/docker-LODK5Y2S.js +0 -16
- package/dist/docker-edge-executor-VHW3JPJS.js +0 -13
- package/dist/edge-import-ABUDKKJI.js +0 -45
- package/dist/ensure-container-edge-SW6HENSG.js +0 -31
- package/dist/nginx-3L4SIBJA.js +0 -24
- package/dist/openresty-lua-4WLGDSER.js +0 -42
- package/dist/setup-NU6T2PDU.js +0 -20
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env sh
|
|
2
|
-
":" //# ; exec "$(command -v
|
|
2
|
+
":" //# ; exec "$(command -v node || command -v bun)" "$0" "$@"
|
|
3
3
|
import { createRequire as __ospCreateRequire } from "node:module";
|
|
4
4
|
const require = __ospCreateRequire(import.meta.url);
|
|
5
5
|
import {
|
|
6
6
|
ENV_KEY,
|
|
7
7
|
ENV_SECRET,
|
|
8
8
|
fromEnv
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-XZYFZ6A5.js";
|
|
10
10
|
import {
|
|
11
11
|
SignatureV4MultiRegion,
|
|
12
12
|
XmlNode,
|
|
13
13
|
XmlShapeDeserializer,
|
|
14
14
|
XmlText
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-QWHBRY2K.js";
|
|
16
16
|
import {
|
|
17
17
|
AwsSdkSigV4ASigner,
|
|
18
18
|
AwsSdkSigV4Signer,
|
|
@@ -28,10 +28,10 @@ import {
|
|
|
28
28
|
resolveAwsSdkSigV4AConfig,
|
|
29
29
|
resolveAwsSdkSigV4Config,
|
|
30
30
|
streamCollector
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-DOD2BXHQ.js";
|
|
32
32
|
import {
|
|
33
33
|
NodeHttpHandler
|
|
34
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-42WQABXM.js";
|
|
35
35
|
import {
|
|
36
36
|
BinaryDecisionDiagram,
|
|
37
37
|
Client,
|
|
@@ -67,7 +67,7 @@ import {
|
|
|
67
67
|
resolveRetryConfig,
|
|
68
68
|
resolveUserAgentConfig,
|
|
69
69
|
setFeature
|
|
70
|
-
} from "./chunk-
|
|
70
|
+
} from "./chunk-DRK3MEDS.js";
|
|
71
71
|
import {
|
|
72
72
|
CredentialsProviderError,
|
|
73
73
|
ENV_PROFILE,
|
|
@@ -98,13 +98,13 @@ import {
|
|
|
98
98
|
resolveHttpHandlerRuntimeConfig,
|
|
99
99
|
resolveRegionConfig,
|
|
100
100
|
toEndpointV1
|
|
101
|
-
} from "./chunk-
|
|
101
|
+
} from "./chunk-3NQQXMDR.js";
|
|
102
102
|
import {
|
|
103
103
|
CloudRuntime
|
|
104
|
-
} from "./chunk-
|
|
104
|
+
} from "./chunk-PSZSKEPA.js";
|
|
105
105
|
import {
|
|
106
106
|
systemCatalog
|
|
107
|
-
} from "./chunk-
|
|
107
|
+
} from "./chunk-GUQ3QCJ7.js";
|
|
108
108
|
import {
|
|
109
109
|
AwsCrc32,
|
|
110
110
|
__awaiter,
|
|
@@ -116,7 +116,7 @@ import {
|
|
|
116
116
|
numToUint8,
|
|
117
117
|
resolveEventStreamSerdeConfig,
|
|
118
118
|
uint32ArrayFrom
|
|
119
|
-
} from "./chunk-
|
|
119
|
+
} from "./chunk-ONKEYSMY.js";
|
|
120
120
|
import {
|
|
121
121
|
Hash,
|
|
122
122
|
NumericValue,
|
|
@@ -134,32 +134,39 @@ import {
|
|
|
134
134
|
toBase64,
|
|
135
135
|
toUint8Array,
|
|
136
136
|
toUtf8
|
|
137
|
-
} from "./chunk-
|
|
137
|
+
} from "./chunk-ZWLDLTJP.js";
|
|
138
138
|
import {
|
|
139
139
|
DockerRuntime,
|
|
140
|
+
daemonConnectionFrom,
|
|
140
141
|
isHostPathSource,
|
|
141
|
-
|
|
142
|
-
|
|
142
|
+
resolveExecExitCode,
|
|
143
|
+
scopedVolumeName,
|
|
144
|
+
startAttachStream,
|
|
145
|
+
startExecStream
|
|
146
|
+
} from "./chunk-3FYIXVX5.js";
|
|
143
147
|
import {
|
|
144
148
|
BareRuntime
|
|
145
|
-
} from "./chunk-
|
|
149
|
+
} from "./chunk-IXM5245F.js";
|
|
146
150
|
import {
|
|
147
151
|
LocalExecutor
|
|
148
|
-
} from "./chunk-
|
|
152
|
+
} from "./chunk-6FH4AXVY.js";
|
|
149
153
|
import {
|
|
150
154
|
EDGE_CONTAINER_NAME,
|
|
151
155
|
edgeCrashReason,
|
|
152
156
|
invalidateEdgeContainer,
|
|
153
157
|
sanitizeEdgeVhosts
|
|
154
|
-
} from "./chunk-
|
|
158
|
+
} from "./chunk-VZ2ZHPS2.js";
|
|
155
159
|
import {
|
|
156
160
|
EDGE_CONTAINER_MOUNTS,
|
|
157
|
-
EDGE_HOST_STATE_DIR
|
|
158
|
-
|
|
161
|
+
EDGE_HOST_STATE_DIR,
|
|
162
|
+
OPENRESTY_DEFAULT_PATHS
|
|
163
|
+
} from "./chunk-OGY63DGD.js";
|
|
159
164
|
import {
|
|
160
165
|
DEFAULT_IMAGE_REGISTRY,
|
|
161
|
-
|
|
162
|
-
|
|
166
|
+
isAwsS3Endpoint,
|
|
167
|
+
safeErrorMessage,
|
|
168
|
+
withTimeout
|
|
169
|
+
} from "./chunk-OTLSJYAC.js";
|
|
163
170
|
|
|
164
171
|
// src/lib/edge-import.ts
|
|
165
172
|
import chalk from "chalk";
|
|
@@ -168,8 +175,42 @@ import ora from "ora";
|
|
|
168
175
|
// ../../packages/adapters/src/runtime/image-transfer.ts
|
|
169
176
|
import { Transform } from "stream";
|
|
170
177
|
|
|
171
|
-
// ../../packages/adapters/src/
|
|
172
|
-
|
|
178
|
+
// ../../packages/adapters/src/infra/edge-baked-conf.ts
|
|
179
|
+
var LOGS_DIR = OPENRESTY_DEFAULT_PATHS.pidPath.replace(/\/[^/]+$/, "");
|
|
180
|
+
|
|
181
|
+
// ../../packages/adapters/src/infra/mail-container.ts
|
|
182
|
+
var MAIL_HOST_STATE_DIR = "/var/lib/openship/mail";
|
|
183
|
+
var MAIL_CONTAINER_MOUNTS = [
|
|
184
|
+
{ host: `${MAIL_HOST_STATE_DIR}/vmail`, container: "/var/vmail" },
|
|
185
|
+
// The Postfix queue. A recreate on an ephemeral layer would drop in-flight and
|
|
186
|
+
// deferred mail without this — keep it on the host.
|
|
187
|
+
{ host: `${MAIL_HOST_STATE_DIR}/spool`, container: "/var/spool/postfix" },
|
|
188
|
+
// DKIM private keys. NEVER baked, never regenerated on pull — a fresh selector
|
|
189
|
+
// breaks DMARC until DNS repropagates.
|
|
190
|
+
{ host: `${MAIL_HOST_STATE_DIR}/dkim`, container: "/var/lib/dkim" },
|
|
191
|
+
// Mutable daemon config the admin layer edits (sasl_passwd, relayhost maps,
|
|
192
|
+
// amavis 50-user). Seeded from baked defaults on first boot.
|
|
193
|
+
{ host: `${MAIL_HOST_STATE_DIR}/config/postfix`, container: "/etc/postfix", seed: true },
|
|
194
|
+
{ host: `${MAIL_HOST_STATE_DIR}/config/dovecot`, container: "/etc/dovecot", seed: true },
|
|
195
|
+
{ host: `${MAIL_HOST_STATE_DIR}/config/amavis`, container: "/etc/amavis/conf.d", seed: true },
|
|
196
|
+
// ClamAV signatures — bind-mounted so a pull doesn't force a multi-hundred-MB
|
|
197
|
+
// freshclam re-download and delay readiness.
|
|
198
|
+
{ host: `${MAIL_HOST_STATE_DIR}/clamav`, container: "/var/lib/clamav" },
|
|
199
|
+
{ host: "/etc/letsencrypt", container: "/etc/letsencrypt", readonly: true }
|
|
200
|
+
];
|
|
201
|
+
var MAIL_DB_HOST_DATA_DIR = `${MAIL_HOST_STATE_DIR}/pgdata`;
|
|
202
|
+
var MAIL_DB_CONTAINER_DATA_DIR = "/var/lib/postgresql/data";
|
|
203
|
+
var MAIL_DB_PGDATA = `${MAIL_DB_CONTAINER_DATA_DIR}/pgdata`;
|
|
204
|
+
var MAIL_HOST_PATHS = {
|
|
205
|
+
saslPasswd: `${MAIL_HOST_STATE_DIR}/config/postfix/sasl_passwd`,
|
|
206
|
+
senderRelayhost: `${MAIL_HOST_STATE_DIR}/config/postfix/sender_relayhost`,
|
|
207
|
+
amavisUserConf: `${MAIL_HOST_STATE_DIR}/config/amavis/50-user`
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// ../../packages/adapters/src/system/mail/ensure-container-mail.ts
|
|
211
|
+
var MAIL_DB_IMAGE = process.env.OPENSHIP_MAIL_DB_IMAGE?.trim() || "postgres:16-alpine";
|
|
212
|
+
var ENGINE_ENV_FILE = `${MAIL_HOST_STATE_DIR}/engine.env`;
|
|
213
|
+
var DB_ENV_FILE = `${MAIL_HOST_STATE_DIR}/db.env`;
|
|
173
214
|
|
|
174
215
|
// ../../packages/adapters/src/system/modules/verify.ts
|
|
175
216
|
import { createHash, createPublicKey, verify as cryptoVerify } from "crypto";
|
|
@@ -243,6 +284,25 @@ function decryptCredential(stored) {
|
|
|
243
284
|
import { createHash as createHash3 } from "crypto";
|
|
244
285
|
import { Transform as Transform2 } from "stream";
|
|
245
286
|
|
|
287
|
+
// ../../packages/adapters/src/backup/common/source-match.ts
|
|
288
|
+
function matchBackupSource(sources, targetSourceId) {
|
|
289
|
+
const wanted = targetSourceId.trim();
|
|
290
|
+
if (!wanted) return null;
|
|
291
|
+
const exact = sources.find((s2) => s2.id === wanted);
|
|
292
|
+
if (exact) return exact;
|
|
293
|
+
const byPhysical = sources.filter((s2) => s2.source === wanted);
|
|
294
|
+
if (byPhysical.length === 1) return byPhysical[0];
|
|
295
|
+
if (byPhysical.length > 1) return null;
|
|
296
|
+
const stripped = wanted.replace(/-\d+$/, "");
|
|
297
|
+
if (stripped !== wanted) {
|
|
298
|
+
const byStripped = sources.filter(
|
|
299
|
+
(s2) => s2.id === stripped || s2.source === stripped
|
|
300
|
+
);
|
|
301
|
+
if (byStripped.length === 1) return byStripped[0];
|
|
302
|
+
}
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
246
306
|
// ../../packages/adapters/src/backup/volume-transfer.ts
|
|
247
307
|
import { Transform as Transform3 } from "stream";
|
|
248
308
|
|
|
@@ -485,8 +545,7 @@ var CustomCommandProducerImpl = class {
|
|
|
485
545
|
kind = "custom_command";
|
|
486
546
|
// No detect — explicit selection only.
|
|
487
547
|
async *produce(service, executor, opts) {
|
|
488
|
-
const
|
|
489
|
-
const produceCommand = cfg.produceCommand ?? opts.command;
|
|
548
|
+
const produceCommand = opts.produceCommand ?? opts.command;
|
|
490
549
|
if (!produceCommand) {
|
|
491
550
|
throw new Error("custom_command producer requires `produceCommand` in policy payload config");
|
|
492
551
|
}
|
|
@@ -496,12 +555,15 @@ var CustomCommandProducerImpl = class {
|
|
|
496
555
|
produceCommand
|
|
497
556
|
]);
|
|
498
557
|
yield {
|
|
499
|
-
name:
|
|
558
|
+
name: opts.artifactName ?? "custom-backup.bin",
|
|
500
559
|
stream: stdout,
|
|
501
560
|
payloadKind: "custom_command",
|
|
502
561
|
metadata: {
|
|
503
562
|
produceCommand,
|
|
504
|
-
|
|
563
|
+
// The ONLY record of how to put this artifact back. Nothing downstream
|
|
564
|
+
// can reconstruct it, which is why losing it here made every captured
|
|
565
|
+
// run unrestorable.
|
|
566
|
+
restoreCommand: opts.restoreCommand ?? null
|
|
505
567
|
}
|
|
506
568
|
};
|
|
507
569
|
const exit = await awaitExit;
|
|
@@ -515,7 +577,7 @@ var CustomCommandProducerImpl = class {
|
|
|
515
577
|
const restoreCommand = artifact.metadata.restoreCommand ?? null;
|
|
516
578
|
if (!restoreCommand) {
|
|
517
579
|
throw new Error(
|
|
518
|
-
"Backup has no restoreCommand recorded \u2014 restore is not possible for this artifact"
|
|
580
|
+
"Backup has no restoreCommand recorded \u2014 restore is not possible for this artifact. Re-run the backup policy to capture a restorable one."
|
|
519
581
|
);
|
|
520
582
|
}
|
|
521
583
|
const body = await artifact.open();
|
|
@@ -582,7 +644,8 @@ var VolumeCopyProducerImpl = class {
|
|
|
582
644
|
const stream = await artifact.open();
|
|
583
645
|
await executor.receiveStream(service, volumeId, stream, {
|
|
584
646
|
compression: artifact.metadata.compression ?? "zstd",
|
|
585
|
-
clearTarget: opts.clearTarget ?? true
|
|
647
|
+
clearTarget: opts.clearTarget ?? true,
|
|
648
|
+
signal: opts.signal
|
|
586
649
|
});
|
|
587
650
|
}
|
|
588
651
|
};
|
|
@@ -595,6 +658,58 @@ registerProducer(VolumeCopyProducer);
|
|
|
595
658
|
// ../../packages/adapters/src/backup/executors/docker.ts
|
|
596
659
|
import { PassThrough } from "stream";
|
|
597
660
|
var HELPER_IMAGE = "alpine:3";
|
|
661
|
+
var DEFAULT_HELPER_IDLE_MS = 10 * 60 * 1e3;
|
|
662
|
+
var DEFAULT_HELPER_TIMEOUT_MS = 6 * 60 * 60 * 1e3;
|
|
663
|
+
var EXIT_POLL_INTERVAL_MS = 2e3;
|
|
664
|
+
function createIdleWatchdog(ms, message) {
|
|
665
|
+
let timer;
|
|
666
|
+
let done = false;
|
|
667
|
+
let fire = () => {
|
|
668
|
+
};
|
|
669
|
+
const promise = new Promise((_resolve, reject) => {
|
|
670
|
+
fire = reject;
|
|
671
|
+
});
|
|
672
|
+
const arm = () => {
|
|
673
|
+
timer = setTimeout(() => {
|
|
674
|
+
done = true;
|
|
675
|
+
fire(new Error(message));
|
|
676
|
+
}, ms);
|
|
677
|
+
timer.unref?.();
|
|
678
|
+
};
|
|
679
|
+
arm();
|
|
680
|
+
return {
|
|
681
|
+
touch: () => {
|
|
682
|
+
if (done) return;
|
|
683
|
+
if (timer) clearTimeout(timer);
|
|
684
|
+
arm();
|
|
685
|
+
},
|
|
686
|
+
promise,
|
|
687
|
+
dispose: () => {
|
|
688
|
+
done = true;
|
|
689
|
+
if (timer) clearTimeout(timer);
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
function createAbortWatch(signal, message) {
|
|
694
|
+
let fire = () => {
|
|
695
|
+
};
|
|
696
|
+
const promise = new Promise((_resolve, reject) => {
|
|
697
|
+
fire = reject;
|
|
698
|
+
});
|
|
699
|
+
const onAbort = () => fire(new Error(message));
|
|
700
|
+
if (signal.aborted) onAbort();
|
|
701
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
702
|
+
return {
|
|
703
|
+
promise,
|
|
704
|
+
dispose: () => signal.removeEventListener("abort", onAbort)
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
function destroyQuietly(stream) {
|
|
708
|
+
try {
|
|
709
|
+
stream?.destroy?.();
|
|
710
|
+
} catch {
|
|
711
|
+
}
|
|
712
|
+
}
|
|
598
713
|
function shellEscape4(s2) {
|
|
599
714
|
return `'${s2.replace(/'/g, "'\\''")}'`;
|
|
600
715
|
}
|
|
@@ -668,12 +783,12 @@ var DockerBackupExecutor = class {
|
|
|
668
783
|
WorkingDir: opts?.cwd,
|
|
669
784
|
Env: opts?.env ? Object.entries(opts.env).map(([k2, v2]) => `${k2}=${v2}`) : void 0
|
|
670
785
|
});
|
|
671
|
-
const stream = await exec.start({
|
|
786
|
+
const stream = await exec.start({ stdin: false });
|
|
672
787
|
return this.attachDemuxed(this.dockerode, exec.id, stream, opts?.timeoutMs);
|
|
673
788
|
}
|
|
674
789
|
async streamPath(service, sourceId, opts) {
|
|
675
790
|
const sources = await this.listSources(service);
|
|
676
|
-
const source = sources
|
|
791
|
+
const source = matchBackupSource(sources, sourceId);
|
|
677
792
|
if (!source) {
|
|
678
793
|
throw new Error(`Backup source "${sourceId}" not found on service ${service.name}`);
|
|
679
794
|
}
|
|
@@ -691,31 +806,52 @@ var DockerBackupExecutor = class {
|
|
|
691
806
|
await this.ensureImage(helperImage);
|
|
692
807
|
const hostConfig = {
|
|
693
808
|
Binds: [`${source.source}:/mnt:ro`],
|
|
694
|
-
|
|
809
|
+
// OFF, for the reason demuxContainerStream already documents but this line
|
|
810
|
+
// used to contradict: AutoRemove lets the daemon reap the helper before
|
|
811
|
+
// container.wait() answers and before the attach stream has drained, which
|
|
812
|
+
// is a 404 on the exit status or a truncated tar. demuxContainerStream
|
|
813
|
+
// removes it once the archive is fully demuxed.
|
|
814
|
+
AutoRemove: false,
|
|
815
|
+
// The archive streams over `attach()` below — the daemon's default
|
|
816
|
+
// json-file driver would ALSO write every byte of it to
|
|
817
|
+
// /var/lib/docker/containers as a log. That's a second copy of the whole
|
|
818
|
+
// backup, and worse than a copy: json-file escapes binary, so a 4.6 GB
|
|
819
|
+
// volume ballooned to a 12 GB log file here. Backups then spike the
|
|
820
|
+
// host's disk by more than the data they read and only release it when
|
|
821
|
+
// the helper exits. Nothing ever reads these logs.
|
|
822
|
+
LogConfig: { Type: "none", Config: {} }
|
|
695
823
|
};
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
824
|
+
return this.handOffHelper(
|
|
825
|
+
{
|
|
826
|
+
Image: helperImage,
|
|
827
|
+
Cmd: ["sh", "-c", compression === "zstd" ? `apk add --no-cache zstd >/dev/null 2>&1; ${tarCmd}` : tarCmd],
|
|
828
|
+
HostConfig: hostConfig,
|
|
829
|
+
AttachStdout: true,
|
|
830
|
+
AttachStderr: true,
|
|
831
|
+
Tty: false,
|
|
832
|
+
// zstd isn't in alpine:3 and is apk-installed at runtime, which needs
|
|
833
|
+
// egress; gzip/none use busybox built-ins and stay network-isolated.
|
|
834
|
+
NetworkDisabled: compression !== "zstd"
|
|
835
|
+
},
|
|
836
|
+
async (helper) => {
|
|
837
|
+
const stream = await helper.attach({
|
|
838
|
+
stream: true,
|
|
839
|
+
stdout: true,
|
|
840
|
+
stderr: true
|
|
841
|
+
});
|
|
842
|
+
await helper.start();
|
|
843
|
+
return this.demuxContainerStream(helper, stream, {
|
|
844
|
+
idleTimeoutMs: opts?.idleTimeoutMs ?? DEFAULT_HELPER_IDLE_MS,
|
|
845
|
+
timeoutMs: opts?.timeoutMs ?? DEFAULT_HELPER_TIMEOUT_MS,
|
|
846
|
+
label: `Backup of "${sourceId}" on service ${service.name}`
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
);
|
|
715
850
|
}
|
|
716
851
|
async receiveStream(service, targetSourceId, body, opts) {
|
|
852
|
+
opts?.signal?.throwIfAborted();
|
|
717
853
|
const sources = await this.listSources(service);
|
|
718
|
-
const source = sources
|
|
854
|
+
const source = matchBackupSource(sources, targetSourceId);
|
|
719
855
|
if (!source) {
|
|
720
856
|
throw new Error(`Restore target "${targetSourceId}" not found on service ${service.name}`);
|
|
721
857
|
}
|
|
@@ -727,49 +863,188 @@ var DockerBackupExecutor = class {
|
|
|
727
863
|
await this.ensureImage(helperImage);
|
|
728
864
|
const clearCmd = opts?.clearTarget ? `find /mnt -mindepth 1 -delete 2>/dev/null || true; ` : "";
|
|
729
865
|
const untarCmd = compression === "zstd" ? `${clearCmd}zstd -d -c | tar -x -C /mnt` : `${clearCmd}tar -x${compressionFlag(compression)}f - -C /mnt`;
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
866
|
+
return this.withHelper(
|
|
867
|
+
{
|
|
868
|
+
Image: helperImage,
|
|
869
|
+
Cmd: [
|
|
870
|
+
"sh",
|
|
871
|
+
"-c",
|
|
872
|
+
compression === "zstd" ? `apk add --no-cache zstd >/dev/null 2>&1; ${untarCmd}` : untarCmd
|
|
873
|
+
],
|
|
874
|
+
// AutoRemove is OFF deliberately, and this is the fix for the `404 no such
|
|
875
|
+
// container` in #434: with it on, the daemon could reap the helper before
|
|
876
|
+
// /containers/{id}/wait answered, and the exit status was then unknowable.
|
|
877
|
+
// withHelper reaps instead — which also plugs a leak AutoRemove never
|
|
878
|
+
// covered, since it only fires for a container that actually started.
|
|
879
|
+
HostConfig: { Binds: [`${source.source}:/mnt`], AutoRemove: false },
|
|
880
|
+
AttachStdin: true,
|
|
881
|
+
AttachStdout: true,
|
|
882
|
+
AttachStderr: true,
|
|
883
|
+
OpenStdin: true,
|
|
884
|
+
StdinOnce: true,
|
|
885
|
+
Tty: false,
|
|
886
|
+
// zstd isn't in alpine:3 and is apk-installed at runtime, which needs
|
|
887
|
+
// egress; gzip/none use busybox built-ins and stay network-isolated.
|
|
888
|
+
NetworkDisabled: compression !== "zstd"
|
|
889
|
+
},
|
|
890
|
+
async (helper) => {
|
|
891
|
+
let stream;
|
|
892
|
+
const idleMs = opts?.idleTimeoutMs ?? DEFAULT_HELPER_IDLE_MS;
|
|
893
|
+
const watchdog = createIdleWatchdog(
|
|
894
|
+
idleMs,
|
|
895
|
+
`Restore of "${targetSourceId}" on service ${service.name} moved no data for ${Math.round(idleMs / 1e3)}s and was abandoned. The extract helper has been removed; the target may hold partial data.`
|
|
896
|
+
);
|
|
897
|
+
const abortWatch = opts?.signal ? createAbortWatch(
|
|
898
|
+
opts.signal,
|
|
899
|
+
`Restore of "${targetSourceId}" on service ${service.name} was cancelled mid-extract. The target holds partial data.`
|
|
900
|
+
) : void 0;
|
|
901
|
+
try {
|
|
902
|
+
stream = await startAttachStream(daemonConnectionFrom(this.dockerode), helper.id, {
|
|
903
|
+
stdin: true,
|
|
904
|
+
stdout: true,
|
|
905
|
+
stderr: true
|
|
906
|
+
});
|
|
907
|
+
await helper.start();
|
|
908
|
+
let bytesWritten = 0;
|
|
909
|
+
let bodyEnded = false;
|
|
910
|
+
body.on("data", (chunk) => {
|
|
911
|
+
bytesWritten += chunk.byteLength;
|
|
912
|
+
watchdog.touch();
|
|
913
|
+
});
|
|
914
|
+
const errChunks = [];
|
|
915
|
+
stream.on("data", (c2) => {
|
|
916
|
+
watchdog.touch();
|
|
917
|
+
if (errChunks.length < 32) errChunks.push(c2);
|
|
918
|
+
});
|
|
919
|
+
body.on("end", () => {
|
|
920
|
+
bodyEnded = true;
|
|
921
|
+
watchdog.touch();
|
|
922
|
+
});
|
|
923
|
+
body.pipe(stream);
|
|
924
|
+
const waitResult = await this.awaitHelperExit(helper, watchdog, () => bodyEnded, {
|
|
925
|
+
timeoutMs: opts?.timeoutMs ?? DEFAULT_HELPER_TIMEOUT_MS,
|
|
926
|
+
label: `Restore of "${targetSourceId}" on service ${service.name}`,
|
|
927
|
+
note: "The target may hold partial data.",
|
|
928
|
+
cancelled: abortWatch?.promise
|
|
929
|
+
});
|
|
930
|
+
if (waitResult.StatusCode !== 0) {
|
|
931
|
+
const detail = Buffer.concat(errChunks).toString("utf8").replace(/[^\x20-\x7e\n]+/g, " ").trim().slice(-500);
|
|
932
|
+
throw new Error(
|
|
933
|
+
`Restore helper exited with code ${waitResult.StatusCode}${detail ? `: ${detail}` : ""}`
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
return { bytesWritten };
|
|
937
|
+
} finally {
|
|
938
|
+
watchdog.dispose();
|
|
939
|
+
abortWatch?.dispose();
|
|
940
|
+
destroyQuietly(stream);
|
|
941
|
+
destroyQuietly(body);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Create a helper, run `fn`, reap the helper however `fn` ends.
|
|
948
|
+
*
|
|
949
|
+
* Every helper here is disposable and every one of them must be removed by us:
|
|
950
|
+
* `AutoRemove` is off on purpose (it races /wait, and it only ever fires for a
|
|
951
|
+
* container that actually started, so a throw between create and start leaked
|
|
952
|
+
* one permanently — the leak behind #434). Four call sites each had their own
|
|
953
|
+
* correct copy of that reap, which is exactly how one of them came to be the
|
|
954
|
+
* one that didn't.
|
|
955
|
+
*
|
|
956
|
+
* NOT for helpers whose output outlives the call: streamPath hands its
|
|
957
|
+
* container to demuxContainerStream, which must reap only after the archive
|
|
958
|
+
* has drained. `handOffHelper` covers that shape.
|
|
959
|
+
*/
|
|
960
|
+
async withHelper(spec, fn) {
|
|
961
|
+
const helper = await this.dockerode.createContainer(spec);
|
|
962
|
+
try {
|
|
963
|
+
return await fn(helper);
|
|
964
|
+
} finally {
|
|
965
|
+
await helper.remove({ force: true }).catch(() => {
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Create a helper whose lifetime outlives this call: reap it only if `setup`
|
|
971
|
+
* throws, then hand ownership to whatever `setup` returned. Without this, a
|
|
972
|
+
* failure between createContainer and the hand-off leaks the container
|
|
973
|
+
* forever — nothing downstream knows it exists yet.
|
|
974
|
+
*/
|
|
975
|
+
async handOffHelper(spec, setup) {
|
|
976
|
+
const helper = await this.dockerode.createContainer(spec);
|
|
977
|
+
try {
|
|
978
|
+
return await setup(helper);
|
|
979
|
+
} catch (err) {
|
|
980
|
+
await helper.remove({ force: true }).catch(() => {
|
|
981
|
+
});
|
|
982
|
+
throw err;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Wait for a helper to exit without trusting any single signal.
|
|
987
|
+
*
|
|
988
|
+
* `helper.wait()` alone is what hung in #434: /containers/{id}/wait is a
|
|
989
|
+
* long-poll with no timeout on either the modem or our transport, so a lost
|
|
990
|
+
* response is indistinguishable from a running container — forever. Three
|
|
991
|
+
* backstops race it:
|
|
992
|
+
*
|
|
993
|
+
* 1. An exit poll that asks the daemon directly. This is what turns the
|
|
994
|
+
* common hang into a few seconds. It's the same "don't trust the attach
|
|
995
|
+
* stream's terminal signal" reasoning already documented in
|
|
996
|
+
* demuxContainerStream; receiveStream was the path without a backstop.
|
|
997
|
+
* 2. The caller's idle watchdog (fed by real traffic, not by this function).
|
|
998
|
+
* 3. An absolute ceiling, so even a helper that somehow keeps trickling
|
|
999
|
+
* bytes cannot run unbounded.
|
|
1000
|
+
*
|
|
1001
|
+
* The poll only starts once the body has been fully written: before that a
|
|
1002
|
+
* not-yet-running helper is normal, and inspecting on a loop would be noise.
|
|
1003
|
+
* It is gated on the body rather than on the attach stream closing because
|
|
1004
|
+
* over an SSH-tunneled attach that close is not reliably delivered — the very
|
|
1005
|
+
* reason this backstop has to exist.
|
|
1006
|
+
*/
|
|
1007
|
+
async awaitHelperExit(helper, watchdog, bodyEnded, opts) {
|
|
1008
|
+
let polling = true;
|
|
1009
|
+
const pollExit = async () => {
|
|
1010
|
+
while (polling) {
|
|
1011
|
+
await new Promise((r2) => {
|
|
1012
|
+
const t8 = setTimeout(r2, EXIT_POLL_INTERVAL_MS);
|
|
1013
|
+
t8.unref?.();
|
|
1014
|
+
});
|
|
1015
|
+
if (!polling || !bodyEnded()) continue;
|
|
1016
|
+
let state;
|
|
1017
|
+
try {
|
|
1018
|
+
state = (await helper.inspect()).State ?? {};
|
|
1019
|
+
} catch (err) {
|
|
1020
|
+
if (err?.statusCode === 404) {
|
|
1021
|
+
throw new Error(
|
|
1022
|
+
`${opts.label}: the helper disappeared before its exit status could be read.${opts.note ? ` ${opts.note}` : ""}`
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
if (state.Running === false && state.Status !== "created" && typeof state.ExitCode === "number") {
|
|
1028
|
+
return { StatusCode: state.ExitCode };
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
return new Promise(() => {
|
|
1032
|
+
});
|
|
1033
|
+
};
|
|
1034
|
+
try {
|
|
1035
|
+
return await withTimeout(
|
|
1036
|
+
Promise.race([
|
|
1037
|
+
helper.wait(),
|
|
1038
|
+
pollExit(),
|
|
1039
|
+
watchdog.promise,
|
|
1040
|
+
...opts.cancelled ? [opts.cancelled] : []
|
|
1041
|
+
]),
|
|
1042
|
+
opts.timeoutMs,
|
|
1043
|
+
`${opts.label} exceeded its ${Math.round(opts.timeoutMs / 1e3)}s ceiling and was abandoned.${opts.note ? ` ${opts.note}` : ""}`
|
|
770
1044
|
);
|
|
1045
|
+
} finally {
|
|
1046
|
+
polling = false;
|
|
771
1047
|
}
|
|
772
|
-
return { bytesWritten };
|
|
773
1048
|
}
|
|
774
1049
|
/**
|
|
775
1050
|
* SAME-DAEMON volume→volume copy in ONE helper container that mounts both
|
|
@@ -783,8 +1058,8 @@ var DockerBackupExecutor = class {
|
|
|
783
1058
|
* network-isolated; source is mounted read-only.
|
|
784
1059
|
*/
|
|
785
1060
|
async copyVolumeLocal(srcService, srcSourceId, dstService, dstSourceId, opts) {
|
|
786
|
-
const src = (await this.listSources(srcService)
|
|
787
|
-
const dst = (await this.listSources(dstService)
|
|
1061
|
+
const src = matchBackupSource(await this.listSources(srcService), srcSourceId);
|
|
1062
|
+
const dst = matchBackupSource(await this.listSources(dstService), dstSourceId);
|
|
788
1063
|
if (!src) throw new Error(`Copy source "${srcSourceId}" not found on ${srcService.name}`);
|
|
789
1064
|
if (!dst) throw new Error(`Copy target "${dstSourceId}" not found on ${dstService.name}`);
|
|
790
1065
|
if (src.type === "tmpfs" || dst.type === "tmpfs") {
|
|
@@ -793,31 +1068,30 @@ var DockerBackupExecutor = class {
|
|
|
793
1068
|
await this.ensureImage(HELPER_IMAGE);
|
|
794
1069
|
const clearCmd = opts?.clearTarget ? "find /to -mindepth 1 -delete 2>/dev/null || true; " : "";
|
|
795
1070
|
const copyCmd = `${clearCmd}{ tar -C /from -cf - . ; echo $? > /tmp/src.rc ; } | tar -C /to -xf - ; rc=$? ; [ "$(cat /tmp/src.rc 2>/dev/null)" = 0 ] && [ "$rc" = 0 ] && du -sk /to 2>/dev/null | cut -f1`;
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1071
|
+
return this.withHelper(
|
|
1072
|
+
{
|
|
1073
|
+
Image: HELPER_IMAGE,
|
|
1074
|
+
Cmd: ["sh", "-c", copyCmd],
|
|
1075
|
+
HostConfig: {
|
|
1076
|
+
Binds: [`${src.source}:/from:ro`, `${dst.source}:/to`]
|
|
1077
|
+
},
|
|
1078
|
+
Tty: true,
|
|
1079
|
+
// merged raw stdout so the trailing `du` number reads cleanly
|
|
1080
|
+
NetworkDisabled: true
|
|
801
1081
|
},
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
);
|
|
1082
|
+
async (helper) => {
|
|
1083
|
+
await helper.start();
|
|
1084
|
+
const res = await helper.wait();
|
|
1085
|
+
const out = await helper.logs({ follow: false, stdout: true, stderr: true }).then((b2) => b2.toString().trim()).catch(() => "");
|
|
1086
|
+
if (res.StatusCode !== 0) {
|
|
1087
|
+
throw new Error(
|
|
1088
|
+
`Local volume copy failed (${srcSourceId}\u2192${dstSourceId}): ${out.slice(0, 500) || `exit ${res.StatusCode}`}`
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
const kb = parseInt(out.split(/\s+/).pop() || "0", 10);
|
|
1092
|
+
return { bytesWritten: Number.isFinite(kb) ? kb * 1024 : 0 };
|
|
814
1093
|
}
|
|
815
|
-
|
|
816
|
-
return { bytesWritten: Number.isFinite(kb) ? kb * 1024 : 0 };
|
|
817
|
-
} finally {
|
|
818
|
-
await helper.remove({ force: true }).catch(() => {
|
|
819
|
-
});
|
|
820
|
-
}
|
|
1094
|
+
);
|
|
821
1095
|
}
|
|
822
1096
|
/**
|
|
823
1097
|
* Does the named volume behind `sourceId` already exist on this daemon, and
|
|
@@ -827,7 +1101,7 @@ var DockerBackupExecutor = class {
|
|
|
827
1101
|
* so we err on the side of NOT clobbering.
|
|
828
1102
|
*/
|
|
829
1103
|
async probeVolume(service, sourceId) {
|
|
830
|
-
const source = (await this.listSources(service)
|
|
1104
|
+
const source = matchBackupSource(await this.listSources(service), sourceId);
|
|
831
1105
|
if (!source || source.type !== "volume" || !source.source) {
|
|
832
1106
|
return { exists: false, empty: true };
|
|
833
1107
|
}
|
|
@@ -837,26 +1111,25 @@ var DockerBackupExecutor = class {
|
|
|
837
1111
|
return { exists: false, empty: true };
|
|
838
1112
|
}
|
|
839
1113
|
await this.ensureImage(HELPER_IMAGE);
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
}
|
|
1114
|
+
return this.withHelper(
|
|
1115
|
+
{
|
|
1116
|
+
Image: HELPER_IMAGE,
|
|
1117
|
+
Cmd: [
|
|
1118
|
+
"sh",
|
|
1119
|
+
"-c",
|
|
1120
|
+
'if [ -z "$(ls -A /probe 2>/dev/null)" ]; then echo VOLEMPTY; else echo VOLDATA; fi'
|
|
1121
|
+
],
|
|
1122
|
+
HostConfig: { Binds: [`${source.source}:/probe:ro`] },
|
|
1123
|
+
Tty: true,
|
|
1124
|
+
NetworkDisabled: true
|
|
1125
|
+
},
|
|
1126
|
+
async (helper) => {
|
|
1127
|
+
await helper.start();
|
|
1128
|
+
await helper.wait();
|
|
1129
|
+
const out = await helper.logs({ follow: false, stdout: true, stderr: true }).then((b2) => b2.toString()).catch(() => "");
|
|
1130
|
+
return { exists: true, empty: out.includes("VOLEMPTY") };
|
|
1131
|
+
}
|
|
1132
|
+
);
|
|
860
1133
|
}
|
|
861
1134
|
async pipeIntoCommand(service, cmd, body, opts) {
|
|
862
1135
|
if (!service.containerId) {
|
|
@@ -874,7 +1147,10 @@ var DockerBackupExecutor = class {
|
|
|
874
1147
|
WorkingDir: opts?.cwd,
|
|
875
1148
|
Env: opts?.env ? Object.entries(opts.env).map(([k2, v2]) => `${k2}=${v2}`) : void 0
|
|
876
1149
|
});
|
|
877
|
-
const stream = await exec.
|
|
1150
|
+
const stream = await startExecStream(daemonConnectionFrom(this.dockerode), exec.id, {
|
|
1151
|
+
tty: false,
|
|
1152
|
+
stdin: true
|
|
1153
|
+
});
|
|
878
1154
|
const stderrChunks = [];
|
|
879
1155
|
const { PassThrough: PassThrough3 } = await import("stream");
|
|
880
1156
|
const stdoutSink = new PassThrough3();
|
|
@@ -906,9 +1182,8 @@ var DockerBackupExecutor = class {
|
|
|
906
1182
|
stream.on("end", async () => {
|
|
907
1183
|
if (timer) clearTimeout(timer);
|
|
908
1184
|
try {
|
|
909
|
-
const info = await this.dockerode.getExec(exec.id).inspect();
|
|
910
1185
|
resolve4({
|
|
911
|
-
code:
|
|
1186
|
+
code: await resolveExecExitCode(exec, exec.id),
|
|
912
1187
|
stderr: Buffer.concat(stderrChunks).toString("utf8").slice(0, 16 * 1024)
|
|
913
1188
|
});
|
|
914
1189
|
} catch (err) {
|
|
@@ -971,9 +1246,8 @@ var DockerBackupExecutor = class {
|
|
|
971
1246
|
stream.on("end", async () => {
|
|
972
1247
|
if (timer) clearTimeout(timer);
|
|
973
1248
|
try {
|
|
974
|
-
const info = await docker.getExec(execId).inspect();
|
|
975
1249
|
resolve4({
|
|
976
|
-
code:
|
|
1250
|
+
code: await resolveExecExitCode(docker.getExec(execId), execId),
|
|
977
1251
|
stderr: Buffer.concat(stderrChunks).toString("utf8").slice(0, 16 * 1024)
|
|
978
1252
|
});
|
|
979
1253
|
} catch (err) {
|
|
@@ -987,7 +1261,7 @@ var DockerBackupExecutor = class {
|
|
|
987
1261
|
});
|
|
988
1262
|
return { stdout, awaitExit };
|
|
989
1263
|
}
|
|
990
|
-
demuxContainerStream(container, stream,
|
|
1264
|
+
demuxContainerStream(container, stream, opts) {
|
|
991
1265
|
const stdout = new PassThrough();
|
|
992
1266
|
const stderrChunks = [];
|
|
993
1267
|
const stderrSink = new PassThrough();
|
|
@@ -995,29 +1269,49 @@ var DockerBackupExecutor = class {
|
|
|
995
1269
|
if (stderrChunks.length < 16) stderrChunks.push(chunk);
|
|
996
1270
|
});
|
|
997
1271
|
container.modem.demuxStream(stream, stdout, stderrSink);
|
|
1272
|
+
let sinksEnded = false;
|
|
1273
|
+
let exited = false;
|
|
1274
|
+
const reapIfDone = () => {
|
|
1275
|
+
if (!sinksEnded || !exited) return;
|
|
1276
|
+
void container.remove({ force: true }).catch(() => {
|
|
1277
|
+
});
|
|
1278
|
+
};
|
|
998
1279
|
const endSinks = () => {
|
|
999
1280
|
stdout.end();
|
|
1000
1281
|
stderrSink.end();
|
|
1282
|
+
sinksEnded = true;
|
|
1283
|
+
reapIfDone();
|
|
1001
1284
|
};
|
|
1002
1285
|
stream.on("end", endSinks);
|
|
1003
1286
|
stream.on("close", endSinks);
|
|
1004
|
-
const
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1287
|
+
const watchdog = createIdleWatchdog(
|
|
1288
|
+
opts.idleTimeoutMs,
|
|
1289
|
+
`${opts.label} produced no data for ${Math.round(opts.idleTimeoutMs / 1e3)}s and was abandoned. Nothing was written; the archive is incomplete and was not kept.`
|
|
1290
|
+
);
|
|
1291
|
+
stream.on("data", () => watchdog.touch());
|
|
1292
|
+
const awaitExit = (async () => {
|
|
1293
|
+
try {
|
|
1294
|
+
const res = await this.awaitHelperExit(container, watchdog, () => sinksEnded, {
|
|
1295
|
+
timeoutMs: opts.timeoutMs,
|
|
1296
|
+
label: opts.label,
|
|
1297
|
+
note: "Nothing was written; the archive is incomplete and was not kept."
|
|
1298
|
+
});
|
|
1299
|
+
exited = true;
|
|
1011
1300
|
setTimeout(endSinks, 3e3);
|
|
1012
|
-
|
|
1301
|
+
reapIfDone();
|
|
1302
|
+
return {
|
|
1013
1303
|
code: res.StatusCode,
|
|
1014
1304
|
stderr: Buffer.concat(stderrChunks).toString("utf8").slice(0, 16 * 1024)
|
|
1305
|
+
};
|
|
1306
|
+
} catch (err) {
|
|
1307
|
+
stdout.destroy(err);
|
|
1308
|
+
void container.remove({ force: true }).catch(() => {
|
|
1015
1309
|
});
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
}
|
|
1020
|
-
});
|
|
1310
|
+
throw err;
|
|
1311
|
+
} finally {
|
|
1312
|
+
watchdog.dispose();
|
|
1313
|
+
}
|
|
1314
|
+
})();
|
|
1021
1315
|
return { stdout, awaitExit };
|
|
1022
1316
|
}
|
|
1023
1317
|
};
|
|
@@ -1259,7 +1553,7 @@ var BareBackupExecutor = class {
|
|
|
1259
1553
|
]);
|
|
1260
1554
|
const path = sourceId;
|
|
1261
1555
|
const tarCmd = compression === "zstd" ? `tar -c -C ${shellEscape6(path)} ${excludeArgs.join(" ")} . | zstd -c -3` : compression === "gzip" ? `tar -cz -C ${shellEscape6(path)} ${excludeArgs.join(" ")} .` : `tar -c -C ${shellEscape6(path)} ${excludeArgs.join(" ")} .`;
|
|
1262
|
-
return this.execStream(service, ["sh", "-c", tarCmd]);
|
|
1556
|
+
return this.execStream(service, ["sh", "-c", tarCmd], { timeoutMs: opts?.timeoutMs });
|
|
1263
1557
|
}
|
|
1264
1558
|
async receiveStream(service, targetSourceId, body, opts) {
|
|
1265
1559
|
const compression = opts?.compression ?? "zstd";
|
|
@@ -1370,7 +1664,7 @@ var LocalDestinationImpl = class {
|
|
|
1370
1664
|
return { ok: false, reason: message };
|
|
1371
1665
|
}
|
|
1372
1666
|
}
|
|
1373
|
-
async put(key, body,
|
|
1667
|
+
async put(key, body, opts) {
|
|
1374
1668
|
const targetPath = this.resolveKey(key);
|
|
1375
1669
|
await fs2.mkdir(dirname(targetPath), { recursive: true });
|
|
1376
1670
|
const tmpPath = `${targetPath}.tmp-${randomBytes3(4).toString("hex")}`;
|
|
@@ -1383,15 +1677,23 @@ var LocalDestinationImpl = class {
|
|
|
1383
1677
|
cb(null, chunk);
|
|
1384
1678
|
}
|
|
1385
1679
|
});
|
|
1680
|
+
let digest;
|
|
1386
1681
|
try {
|
|
1387
1682
|
await pipelineP(body, counterAndHasher, createWriteStream2(tmpPath));
|
|
1683
|
+
digest = hash.digest("hex");
|
|
1684
|
+
const expected = opts.sha256?.trim().replace(/^sha256:/i, "").toLowerCase();
|
|
1685
|
+
if (expected && expected !== digest) {
|
|
1686
|
+
throw new Error(
|
|
1687
|
+
`sha256 mismatch writing ${key}: caller declared ${expected}, bytes hashed to ${digest}`
|
|
1688
|
+
);
|
|
1689
|
+
}
|
|
1388
1690
|
await fs2.rename(tmpPath, targetPath);
|
|
1389
1691
|
} catch (err) {
|
|
1390
1692
|
await fs2.unlink(tmpPath).catch(() => {
|
|
1391
1693
|
});
|
|
1392
1694
|
throw err;
|
|
1393
1695
|
}
|
|
1394
|
-
return { bytesWritten, etag:
|
|
1696
|
+
return { bytesWritten, etag: digest };
|
|
1395
1697
|
}
|
|
1396
1698
|
async get(key) {
|
|
1397
1699
|
const path = this.resolveKey(key);
|
|
@@ -10976,10 +11278,10 @@ var package_default = {
|
|
|
10976
11278
|
// ../../node_modules/@aws-sdk/credential-provider-node/dist-es/remoteProvider.js
|
|
10977
11279
|
var ENV_IMDS_DISABLED = "AWS_EC2_METADATA_DISABLED";
|
|
10978
11280
|
var remoteProvider = async (init) => {
|
|
10979
|
-
const { ENV_CMDS_FULL_URI, ENV_CMDS_RELATIVE_URI, fromContainerMetadata, fromInstanceMetadata } = await import("./dist-es-
|
|
11281
|
+
const { ENV_CMDS_FULL_URI, ENV_CMDS_RELATIVE_URI, fromContainerMetadata, fromInstanceMetadata } = await import("./dist-es-IOH6G4GM.js");
|
|
10980
11282
|
if (process.env[ENV_CMDS_RELATIVE_URI] || process.env[ENV_CMDS_FULL_URI]) {
|
|
10981
11283
|
init.logger?.debug("@aws-sdk/credential-provider-node - remoteProvider::fromHttp/fromContainerMetadata");
|
|
10982
|
-
const { fromHttp } = await import("./dist-es-
|
|
11284
|
+
const { fromHttp } = await import("./dist-es-UYDDOVZ3.js");
|
|
10983
11285
|
return chain(fromHttp(init), fromContainerMetadata(init));
|
|
10984
11286
|
}
|
|
10985
11287
|
if (process.env[ENV_IMDS_DISABLED] && process.env[ENV_IMDS_DISABLED] !== "false") {
|
|
@@ -11091,22 +11393,22 @@ var defaultProvider = (init = {}) => memoizeChain([
|
|
|
11091
11393
|
if (!ssoStartUrl && !ssoAccountId && !ssoRegion && !ssoRoleName && !ssoSession) {
|
|
11092
11394
|
throw new CredentialsProviderError("Skipping SSO provider in default chain (inputs do not include SSO fields).", { logger: init.logger });
|
|
11093
11395
|
}
|
|
11094
|
-
const { fromSSO } = await import("./dist-es-
|
|
11396
|
+
const { fromSSO } = await import("./dist-es-XUOR4NQN.js");
|
|
11095
11397
|
return fromSSO(init)(awsIdentityProperties);
|
|
11096
11398
|
},
|
|
11097
11399
|
async (awsIdentityProperties) => {
|
|
11098
11400
|
init.logger?.debug("@aws-sdk/credential-provider-node - defaultProvider::fromIni");
|
|
11099
|
-
const { fromIni } = await import("./dist-es-
|
|
11401
|
+
const { fromIni } = await import("./dist-es-JGBPABJI.js");
|
|
11100
11402
|
return fromIni(init)(awsIdentityProperties);
|
|
11101
11403
|
},
|
|
11102
11404
|
async (awsIdentityProperties) => {
|
|
11103
11405
|
init.logger?.debug("@aws-sdk/credential-provider-node - defaultProvider::fromProcess");
|
|
11104
|
-
const { fromProcess } = await import("./dist-es-
|
|
11406
|
+
const { fromProcess } = await import("./dist-es-CCVZEFRZ.js");
|
|
11105
11407
|
return fromProcess(init)(awsIdentityProperties);
|
|
11106
11408
|
},
|
|
11107
11409
|
async (awsIdentityProperties) => {
|
|
11108
11410
|
init.logger?.debug("@aws-sdk/credential-provider-node - defaultProvider::fromTokenFile");
|
|
11109
|
-
const { fromTokenFile } = await import("./dist-es-
|
|
11411
|
+
const { fromTokenFile } = await import("./dist-es-2YRDXMUU.js");
|
|
11110
11412
|
return fromTokenFile(init)(awsIdentityProperties);
|
|
11111
11413
|
},
|
|
11112
11414
|
async () => {
|
|
@@ -12191,7 +12493,7 @@ var S3DestinationImpl = class {
|
|
|
12191
12493
|
if (!accessKeyId || !secretAccessKey) {
|
|
12192
12494
|
throw new Error(`S3Destination "${row.name}" missing access credentials`);
|
|
12193
12495
|
}
|
|
12194
|
-
const isAws =
|
|
12496
|
+
const isAws = isAwsS3Endpoint(row.endpoint);
|
|
12195
12497
|
this.client = new S3Client({
|
|
12196
12498
|
endpoint: row.endpoint ?? void 0,
|
|
12197
12499
|
region: row.region ?? "auto",
|
|
@@ -12668,11 +12970,11 @@ registerDestination("sftp", (row) => new SftpDestinationImpl(row));
|
|
|
12668
12970
|
registerDestination("openship_server", (row) => new SftpDestinationImpl(row));
|
|
12669
12971
|
|
|
12670
12972
|
// src/lib/compose.ts
|
|
12671
|
-
import { spawnSync as spawnSync3 } from "child_process";
|
|
12973
|
+
import { spawn as spawn2, spawnSync as spawnSync3 } from "child_process";
|
|
12672
12974
|
import { randomBytes as randomBytes5 } from "crypto";
|
|
12673
|
-
import { chmodSync, existsSync as
|
|
12975
|
+
import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
12674
12976
|
import { homedir as homedir2, userInfo } from "os";
|
|
12675
|
-
import { join as
|
|
12977
|
+
import { join as join7 } from "path";
|
|
12676
12978
|
|
|
12677
12979
|
// src/lib/paths.ts
|
|
12678
12980
|
import { homedir } from "os";
|
|
@@ -12681,17 +12983,193 @@ var DEFAULT_HOME = join3(homedir(), ".openship");
|
|
|
12681
12983
|
var override = process.env.OPENSHIP_HOME?.trim();
|
|
12682
12984
|
var OS_DIR = override ? resolve2(override) : DEFAULT_HOME;
|
|
12683
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
|
+
});
|
|
13008
|
+
}
|
|
13009
|
+
async function waitPortFree(port, opts = {}) {
|
|
13010
|
+
const timeoutMs = opts.timeoutMs ?? 6e3;
|
|
13011
|
+
const intervalMs = opts.intervalMs ?? 250;
|
|
13012
|
+
const host = opts.host ?? LOOPBACK;
|
|
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
|
+
}
|
|
13019
|
+
}
|
|
13020
|
+
function getFreePort(host = LOOPBACK) {
|
|
13021
|
+
return new Promise((resolve4, reject) => {
|
|
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
|
+
});
|
|
13030
|
+
}
|
|
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
|
+
}
|
|
12684
13160
|
|
|
12685
13161
|
// src/lib/source-install.ts
|
|
12686
13162
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
12687
|
-
import { existsSync as
|
|
12688
|
-
import { join as
|
|
13163
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
13164
|
+
import { join as join6 } from "path";
|
|
12689
13165
|
|
|
12690
13166
|
// src/lib/from-source.ts
|
|
12691
13167
|
import { spawn, spawnSync } from "child_process";
|
|
12692
|
-
import { existsSync, mkdirSync } from "fs";
|
|
12693
|
-
import { join as
|
|
13168
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
13169
|
+
import { join as join5, resolve as resolve3 } from "path";
|
|
12694
13170
|
var DEFAULT_REPO = "https://github.com/oblien/openship.git";
|
|
13171
|
+
var SOURCE_CHECKOUT_DIR = join5(OS_DIR, "src");
|
|
13172
|
+
var SOURCE_DIST_DIR = join5(OS_DIR, "from-source-dist");
|
|
12695
13173
|
function has(cmd) {
|
|
12696
13174
|
try {
|
|
12697
13175
|
return spawnSync(cmd, ["--version"], { stdio: "ignore" }).status === 0;
|
|
@@ -12720,7 +13198,7 @@ function shortSha(cwd) {
|
|
|
12720
13198
|
return r2.status === 0 ? (r2.stdout ?? "").trim() || "unknown" : "unknown";
|
|
12721
13199
|
}
|
|
12722
13200
|
function isMonorepo(dir) {
|
|
12723
|
-
return
|
|
13201
|
+
return existsSync2(join5(dir, "package.json")) && existsSync2(join5(dir, "apps/api/package.json")) && existsSync2(join5(dir, "apps/dashboard/package.json"));
|
|
12724
13202
|
}
|
|
12725
13203
|
async function prepareFromSource(opts) {
|
|
12726
13204
|
if (!has("bun")) {
|
|
@@ -12744,9 +13222,9 @@ async function prepareFromSource(opts) {
|
|
|
12744
13222
|
}
|
|
12745
13223
|
ref = (opts.ref || "main").trim();
|
|
12746
13224
|
const repoUrl = opts.repo || DEFAULT_REPO;
|
|
12747
|
-
sourceDir =
|
|
12748
|
-
|
|
12749
|
-
if (!
|
|
13225
|
+
sourceDir = SOURCE_CHECKOUT_DIR;
|
|
13226
|
+
mkdirSync2(OS_DIR, { recursive: true });
|
|
13227
|
+
if (!existsSync2(join5(sourceDir, ".git"))) {
|
|
12750
13228
|
console.log(` Cloning ${repoUrl} \u2192 ${sourceDir}`);
|
|
12751
13229
|
await run("git", ["clone", repoUrl, sourceDir], OS_DIR);
|
|
12752
13230
|
}
|
|
@@ -12761,29 +13239,29 @@ async function prepareFromSource(opts) {
|
|
|
12761
13239
|
console.log(` Source: ${sourceDir} @ ${ref} (${sha})`);
|
|
12762
13240
|
console.log(" Installing workspace dependencies (bun install)\u2026");
|
|
12763
13241
|
await run("bun", ["install"], sourceDir);
|
|
12764
|
-
const distDir =
|
|
13242
|
+
const distDir = SOURCE_DIST_DIR;
|
|
12765
13243
|
console.log(" Building release dist (compiles the dashboard \u2014 needs RAM/CPU)\u2026");
|
|
12766
13244
|
await run(
|
|
12767
13245
|
"bun",
|
|
12768
|
-
["run",
|
|
13246
|
+
["run", join5(sourceDir, "apps/api/scripts/build-release.ts")],
|
|
12769
13247
|
sourceDir,
|
|
12770
13248
|
{ DIST_DIR: distDir, NODE_ENV: "production", CLOUD_MODE: "false", OPENSHIP_TARGET: "local" }
|
|
12771
13249
|
);
|
|
12772
13250
|
console.log(" Installing runtime dependencies in the dist\u2026");
|
|
12773
13251
|
await run("bun", ["install", "--production", "--frozen-lockfile"], distDir);
|
|
12774
|
-
const apiDir =
|
|
12775
|
-
const dashboardDir =
|
|
12776
|
-
if (!
|
|
13252
|
+
const apiDir = join5(distDir, "api");
|
|
13253
|
+
const dashboardDir = join5(distDir, "dashboard");
|
|
13254
|
+
if (!existsSync2(join5(apiDir, "src/index.ts"))) {
|
|
12777
13255
|
throw new Error(`Build produced no API at ${apiDir}/src/index.ts \u2014 build-release layout drift?`);
|
|
12778
13256
|
}
|
|
12779
|
-
if (!
|
|
13257
|
+
if (!existsSync2(join5(dashboardDir, "apps/dashboard/server.js"))) {
|
|
12780
13258
|
throw new Error(`Build produced no dashboard at ${dashboardDir}/apps/dashboard/server.js.`);
|
|
12781
13259
|
}
|
|
12782
13260
|
return { apiDir, dashboardDir, ref, sha, sourceDir };
|
|
12783
13261
|
}
|
|
12784
13262
|
|
|
12785
13263
|
// src/lib/source-install.ts
|
|
12786
|
-
var MARKER =
|
|
13264
|
+
var MARKER = join6(OS_DIR, "source-install.json");
|
|
12787
13265
|
function parseSourceInstall(raw) {
|
|
12788
13266
|
try {
|
|
12789
13267
|
const obj = JSON.parse(raw);
|
|
@@ -12794,9 +13272,9 @@ function parseSourceInstall(raw) {
|
|
|
12794
13272
|
}
|
|
12795
13273
|
}
|
|
12796
13274
|
function readSourceInstall() {
|
|
12797
|
-
if (!
|
|
13275
|
+
if (!existsSync3(MARKER)) return null;
|
|
12798
13276
|
try {
|
|
12799
|
-
return parseSourceInstall(
|
|
13277
|
+
return parseSourceInstall(readFileSync2(MARKER, "utf8"));
|
|
12800
13278
|
} catch {
|
|
12801
13279
|
return null;
|
|
12802
13280
|
}
|
|
@@ -12817,7 +13295,7 @@ async function rebuildFromSource(info) {
|
|
|
12817
13295
|
if (!has("git")) {
|
|
12818
13296
|
throw new Error("`git` is required to update the source checkout but wasn't found on PATH.");
|
|
12819
13297
|
}
|
|
12820
|
-
if (!
|
|
13298
|
+
if (!existsSync3(join6(info.dir, ".git"))) {
|
|
12821
13299
|
throw new Error(
|
|
12822
13300
|
`Source checkout ${info.dir} is missing (no .git) \u2014 reinstall from source with scripts/install-source.sh.`
|
|
12823
13301
|
);
|
|
@@ -12826,7 +13304,7 @@ async function rebuildFromSource(info) {
|
|
|
12826
13304
|
await run("git", ["checkout", info.ref], info.dir);
|
|
12827
13305
|
await run("git", ["pull", "--ff-only", "origin", info.ref], info.dir).catch(() => {
|
|
12828
13306
|
});
|
|
12829
|
-
const cliDir =
|
|
13307
|
+
const cliDir = join6(info.dir, "apps/cli");
|
|
12830
13308
|
await run("bun", ["install"], info.dir);
|
|
12831
13309
|
await run("bun", ["run", "build"], cliDir);
|
|
12832
13310
|
await run("bun", ["run", "build/stage-dashboard.ts"], cliDir);
|
|
@@ -12839,11 +13317,11 @@ var EDGE_ACME_HOST_DIR = `${EDGE_HOST_STATE_DIR}/acme`;
|
|
|
12839
13317
|
function edgeVolumeYaml(indent) {
|
|
12840
13318
|
return EDGE_CONTAINER_MOUNTS.map((m2) => `${indent}- ${m2.host}:${m2.container}:z`).join("\n");
|
|
12841
13319
|
}
|
|
12842
|
-
var COMPOSE_DIR =
|
|
12843
|
-
var INSTALL_METHOD_FILE =
|
|
12844
|
-
var COMPOSE_FILE =
|
|
12845
|
-
var BUILD_FILE =
|
|
12846
|
-
var ENV_FILE =
|
|
13320
|
+
var COMPOSE_DIR = join7(OS_DIR, "compose");
|
|
13321
|
+
var INSTALL_METHOD_FILE = join7(OS_DIR, "install-method");
|
|
13322
|
+
var COMPOSE_FILE = join7(COMPOSE_DIR, "docker-compose.yml");
|
|
13323
|
+
var BUILD_FILE = join7(COMPOSE_DIR, "docker-compose.build.yml");
|
|
13324
|
+
var ENV_FILE = join7(COMPOSE_DIR, ".env");
|
|
12847
13325
|
var BUILT_SERVICES = [
|
|
12848
13326
|
{ service: "api", dockerfile: "apps/api/Dockerfile" },
|
|
12849
13327
|
{ service: "dashboard", dockerfile: "apps/dashboard/Dockerfile" },
|
|
@@ -12852,24 +13330,45 @@ var BUILT_SERVICES = [
|
|
|
12852
13330
|
var ENV_CONSUMING_SERVICES = ["api", "dashboard", "edge"];
|
|
12853
13331
|
function readInstallMethod() {
|
|
12854
13332
|
try {
|
|
12855
|
-
const v2 =
|
|
13333
|
+
const v2 = readFileSync3(INSTALL_METHOD_FILE, "utf8").trim();
|
|
12856
13334
|
return v2 === "compose" || v2 === "bare" ? v2 : null;
|
|
12857
13335
|
} catch {
|
|
12858
13336
|
return null;
|
|
12859
13337
|
}
|
|
12860
13338
|
}
|
|
12861
13339
|
function writeInstallMethod(method) {
|
|
12862
|
-
|
|
12863
|
-
|
|
13340
|
+
mkdirSync3(OS_DIR, { recursive: true, mode: 448 });
|
|
13341
|
+
writeFileSync2(INSTALL_METHOD_FILE, method, { mode: 384 });
|
|
12864
13342
|
}
|
|
12865
|
-
function
|
|
12866
|
-
const
|
|
12867
|
-
if (
|
|
12868
|
-
|
|
12869
|
-
return compose2.status === 0;
|
|
13343
|
+
function dockerState() {
|
|
13344
|
+
const ok = (args) => spawnSync3("docker", args, { stdio: "ignore" }).status === 0;
|
|
13345
|
+
if (!ok(["--version"])) return { binary: false, plugin: false, daemon: false };
|
|
13346
|
+
return { binary: true, plugin: ok(["compose", "version"]), daemon: ok(["version"]) };
|
|
12870
13347
|
}
|
|
12871
|
-
function
|
|
12872
|
-
|
|
13348
|
+
function dockerGap(state = dockerState()) {
|
|
13349
|
+
if (!state.binary) {
|
|
13350
|
+
return { summary: "Docker isn't installed", installable: true };
|
|
13351
|
+
}
|
|
13352
|
+
if (!state.plugin) {
|
|
13353
|
+
return {
|
|
13354
|
+
summary: "Docker is installed but the Compose plugin (`docker compose`) is missing",
|
|
13355
|
+
installable: true
|
|
13356
|
+
};
|
|
13357
|
+
}
|
|
13358
|
+
if (!state.daemon) {
|
|
13359
|
+
const asRoot = typeof process.getuid === "function" && process.getuid() === 0;
|
|
13360
|
+
return {
|
|
13361
|
+
summary: "Docker is installed but its daemon isn't reachable",
|
|
13362
|
+
// Reinstalling changes nothing: a group change only applies to NEW logins,
|
|
13363
|
+
// and a stopped daemon needs starting, not installing.
|
|
13364
|
+
installable: false,
|
|
13365
|
+
hint: asRoot ? "Start it with: systemctl start docker" : `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: sudo systemctl start docker`
|
|
13366
|
+
};
|
|
13367
|
+
}
|
|
13368
|
+
return null;
|
|
13369
|
+
}
|
|
13370
|
+
function hasDockerCompose() {
|
|
13371
|
+
return dockerGap() === null;
|
|
12873
13372
|
}
|
|
12874
13373
|
function hasCmd(cmd) {
|
|
12875
13374
|
return spawnSync3(cmd, ["--version"], { stdio: "ignore" }).status === 0;
|
|
@@ -12877,22 +13376,67 @@ function hasCmd(cmd) {
|
|
|
12877
13376
|
function shQuote(s2) {
|
|
12878
13377
|
return `'${s2.replace(/'/g, `'\\''`)}'`;
|
|
12879
13378
|
}
|
|
12880
|
-
|
|
12881
|
-
|
|
12882
|
-
if (
|
|
13379
|
+
function dockerInstallPreview(state = dockerState()) {
|
|
13380
|
+
const gap = dockerGap(state);
|
|
13381
|
+
if (!gap) return { state, gap: null, wouldInstall: false };
|
|
13382
|
+
if (process.platform !== "linux" || !gap.installable) return { state, gap, wouldInstall: false };
|
|
12883
13383
|
const plan = systemCatalog.installs.docker({
|
|
12884
13384
|
os: "linux",
|
|
12885
13385
|
serviceManager: "systemd"
|
|
12886
13386
|
});
|
|
12887
|
-
if (!plan.supported || !plan.installCommand) return false;
|
|
13387
|
+
if (!plan.supported || !plan.installCommand) return { state, gap, wouldInstall: false };
|
|
13388
|
+
return {
|
|
13389
|
+
state,
|
|
13390
|
+
gap,
|
|
13391
|
+
wouldInstall: true,
|
|
13392
|
+
installCommand: plan.installCommand,
|
|
13393
|
+
...plan.startCommand ? { startCommand: plan.startCommand } : {}
|
|
13394
|
+
};
|
|
13395
|
+
}
|
|
13396
|
+
async function ensureDocker(opts = {}) {
|
|
13397
|
+
const notice = opts.onNotice ?? ((line) => process.stderr.write(` ${line}
|
|
13398
|
+
`));
|
|
13399
|
+
const { state, gap, wouldInstall, installCommand, startCommand } = dockerInstallPreview();
|
|
13400
|
+
if (!gap) return true;
|
|
13401
|
+
if (process.platform !== "linux") return false;
|
|
13402
|
+
if (!gap.installable) {
|
|
13403
|
+
notice(gap.summary + ".");
|
|
13404
|
+
if (gap.hint) notice(gap.hint);
|
|
13405
|
+
return false;
|
|
13406
|
+
}
|
|
13407
|
+
if (!wouldInstall || !installCommand) return false;
|
|
12888
13408
|
const asRoot = typeof process.getuid === "function" && process.getuid() === 0;
|
|
12889
13409
|
const sudo = !asRoot && hasCmd("sudo") ? "sudo " : "";
|
|
12890
|
-
|
|
12891
|
-
|
|
12892
|
-
|
|
12893
|
-
|
|
12894
|
-
|
|
12895
|
-
|
|
13410
|
+
notice("This can take 2-5 minutes on a small VPS (~150 MB of packages) and stays quiet while apt works.");
|
|
13411
|
+
if (state.binary) {
|
|
13412
|
+
notice("Docker's installer will warn that docker already exists and pause ~20s before continuing \u2014 that's expected.");
|
|
13413
|
+
}
|
|
13414
|
+
const sh = (script) => new Promise((resolve4) => {
|
|
13415
|
+
const child = spawn2("sh", ["-c", sudo ? `${sudo}sh -c ${shQuote(script)}` : script], {
|
|
13416
|
+
stdio: "inherit"
|
|
13417
|
+
});
|
|
13418
|
+
const started = Date.now();
|
|
13419
|
+
const tick = setInterval(() => {
|
|
13420
|
+
const s2 = Math.round((Date.now() - started) / 1e3);
|
|
13421
|
+
notice(`still installing Docker \u2014 ${Math.floor(s2 / 60)}m${String(s2 % 60).padStart(2, "0")}s elapsed\u2026`);
|
|
13422
|
+
}, 3e4);
|
|
13423
|
+
const done = (code) => {
|
|
13424
|
+
clearInterval(tick);
|
|
13425
|
+
resolve4(code);
|
|
13426
|
+
};
|
|
13427
|
+
child.on("error", () => done(1));
|
|
13428
|
+
child.on("close", (code) => done(code ?? 1));
|
|
13429
|
+
});
|
|
13430
|
+
if (await sh(installCommand) !== 0) return false;
|
|
13431
|
+
if (startCommand) await sh(startCommand);
|
|
13432
|
+
const after = dockerGap();
|
|
13433
|
+
if (!after) {
|
|
13434
|
+
notice("Docker ready.");
|
|
13435
|
+
return true;
|
|
13436
|
+
}
|
|
13437
|
+
notice(after.summary + ".");
|
|
13438
|
+
if (after.hint) notice(after.hint);
|
|
13439
|
+
return false;
|
|
12896
13440
|
}
|
|
12897
13441
|
var COMPOSE_YAML = `# Managed by \`openship up\` \u2014 do not edit; re-run \`openship up\` to regenerate.
|
|
12898
13442
|
services:
|
|
@@ -12900,6 +13444,12 @@ services:
|
|
|
12900
13444
|
image: postgres:16-alpine
|
|
12901
13445
|
restart: unless-stopped
|
|
12902
13446
|
environment:
|
|
13447
|
+
# Keep the data dir in a subdirectory of the volume so a fresh install never
|
|
13448
|
+
# runs initdb against a bare mount root (which fails on quirky host
|
|
13449
|
+
# filesystems with EPERM \u2014 #350). OPENSHIP_PGDATA is decided ONCE at install
|
|
13450
|
+
# by the CLI (fresh \u2192 subdir, pre-existing volume \u2192 root) and preserved in
|
|
13451
|
+
# .env, so this never moves an existing database.
|
|
13452
|
+
PGDATA: \${OPENSHIP_PGDATA:-/var/lib/postgresql/data/pgdata}
|
|
12903
13453
|
POSTGRES_USER: \${POSTGRES_USER:-openship}
|
|
12904
13454
|
POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:?missing from .env \u2014 re-run openship up to regenerate it}
|
|
12905
13455
|
POSTGRES_DB: \${POSTGRES_DB:-openship}
|
|
@@ -12926,7 +13476,10 @@ services:
|
|
|
12926
13476
|
api:
|
|
12927
13477
|
image: \${OPENSHIP_IMAGE_REGISTRY:-ghcr.io/oblien}/openship-api:\${OPENSHIP_VERSION:-latest}
|
|
12928
13478
|
restart: unless-stopped
|
|
12929
|
-
|
|
13479
|
+
# Loopback by default \u2014 the host-net edge reaches it over loopback, so nothing
|
|
13480
|
+
# sits on a public interface. OPENSHIP_BIND_ADDR opts into a public/LAN interface
|
|
13481
|
+
# (set by \`openship up\` when a public URL is configured for off-box access).
|
|
13482
|
+
ports: ["\${OPENSHIP_BIND_ADDR:-127.0.0.1}:\${API_PORT:-4000}:\${API_PORT:-4000}"]
|
|
12930
13483
|
volumes:
|
|
12931
13484
|
- /var/run/docker.sock:/var/run/docker.sock
|
|
12932
13485
|
# Routing state shared with the edge, as HOST BIND MOUNTS (generated from
|
|
@@ -12961,7 +13514,8 @@ ${edgeVolumeYaml(" ")}
|
|
|
12961
13514
|
dashboard:
|
|
12962
13515
|
image: \${OPENSHIP_IMAGE_REGISTRY:-ghcr.io/oblien}/openship-dashboard:\${OPENSHIP_VERSION:-latest}
|
|
12963
13516
|
restart: unless-stopped
|
|
12964
|
-
|
|
13517
|
+
# Loopback by default (see api note); OPENSHIP_BIND_ADDR opts into a public interface.
|
|
13518
|
+
ports: ["\${OPENSHIP_BIND_ADDR:-127.0.0.1}:\${DASHBOARD_PORT:-3001}:\${DASHBOARD_PORT:-3001}"]
|
|
12965
13519
|
env_file: [.env]
|
|
12966
13520
|
environment:
|
|
12967
13521
|
NODE_ENV: production
|
|
@@ -12995,7 +13549,7 @@ function keepSecret(existing, key) {
|
|
|
12995
13549
|
function readEnvFile() {
|
|
12996
13550
|
const out = {};
|
|
12997
13551
|
try {
|
|
12998
|
-
for (const line of
|
|
13552
|
+
for (const line of readFileSync3(ENV_FILE, "utf8").split("\n")) {
|
|
12999
13553
|
const m2 = line.match(/^([A-Z0-9_]+)=(.*)$/);
|
|
13000
13554
|
if (m2) out[m2[1]] = m2[2];
|
|
13001
13555
|
}
|
|
@@ -13012,13 +13566,23 @@ function rewriteHostAuthorizedKeys(existing, pub) {
|
|
|
13012
13566
|
const kept = existing.split("\n").filter((line) => line.trim() && !line.includes(HOST_KEY_COMMENT));
|
|
13013
13567
|
return [...kept, hostKeyAuthLine(pub)].join("\n") + "\n";
|
|
13014
13568
|
}
|
|
13015
|
-
function
|
|
13016
|
-
if (process.platform !== "linux") return null;
|
|
13569
|
+
function plannedHostChannel(hostControl) {
|
|
13570
|
+
if (!hostControl || process.platform !== "linux") return null;
|
|
13571
|
+
let user;
|
|
13572
|
+
try {
|
|
13573
|
+
user = userInfo().username;
|
|
13574
|
+
} catch {
|
|
13575
|
+
user = process.env.USER || process.env.LOGNAME || "root";
|
|
13576
|
+
}
|
|
13577
|
+
return { user, keyPath: join7(COMPOSE_DIR, "host-ssh", "id_ed25519") };
|
|
13578
|
+
}
|
|
13579
|
+
function provisionHostSshChannel(hostControl) {
|
|
13580
|
+
const target = plannedHostChannel(hostControl);
|
|
13581
|
+
if (!target) return null;
|
|
13582
|
+
const { user, keyPath } = target;
|
|
13017
13583
|
try {
|
|
13018
|
-
|
|
13019
|
-
|
|
13020
|
-
const keyPath = join6(sshDir, "id_ed25519");
|
|
13021
|
-
if (!existsSync3(keyPath)) {
|
|
13584
|
+
mkdirSync3(join7(COMPOSE_DIR, "host-ssh"), { recursive: true, mode: 448 });
|
|
13585
|
+
if (!existsSync4(keyPath)) {
|
|
13022
13586
|
const g2 = spawnSync3(
|
|
13023
13587
|
"ssh-keygen",
|
|
13024
13588
|
["-t", "ed25519", "-N", "", "-q", "-f", keyPath, "-C", HOST_KEY_COMMENT],
|
|
@@ -13026,21 +13590,14 @@ function provisionHostSshChannel() {
|
|
|
13026
13590
|
);
|
|
13027
13591
|
if (g2.status !== 0) return null;
|
|
13028
13592
|
}
|
|
13029
|
-
const pub =
|
|
13593
|
+
const pub = readFileSync3(`${keyPath}.pub`, "utf8").trim();
|
|
13030
13594
|
if (!pub) return null;
|
|
13031
|
-
const
|
|
13032
|
-
|
|
13033
|
-
|
|
13034
|
-
|
|
13035
|
-
return process.env.USER || process.env.LOGNAME || "root";
|
|
13036
|
-
}
|
|
13037
|
-
})();
|
|
13038
|
-
const userSshDir = join6(homedir2(), ".ssh");
|
|
13039
|
-
mkdirSync2(userSshDir, { recursive: true, mode: 448 });
|
|
13040
|
-
const authKeys = join6(userSshDir, "authorized_keys");
|
|
13041
|
-
const existing = existsSync3(authKeys) ? readFileSync2(authKeys, "utf8") : "";
|
|
13595
|
+
const userSshDir = join7(homedir2(), ".ssh");
|
|
13596
|
+
mkdirSync3(userSshDir, { recursive: true, mode: 448 });
|
|
13597
|
+
const authKeys = join7(userSshDir, "authorized_keys");
|
|
13598
|
+
const existing = existsSync4(authKeys) ? readFileSync3(authKeys, "utf8") : "";
|
|
13042
13599
|
const next = rewriteHostAuthorizedKeys(existing, pub);
|
|
13043
|
-
if (next !== existing)
|
|
13600
|
+
if (next !== existing) writeFileSync2(authKeys, next, { mode: 384 });
|
|
13044
13601
|
chmodSync(authKeys, 384);
|
|
13045
13602
|
return { user, keyPath };
|
|
13046
13603
|
} catch {
|
|
@@ -13051,6 +13608,11 @@ function composeProjectName(prev) {
|
|
|
13051
13608
|
if (prev.COMPOSE_PROJECT_NAME) return prev.COMPOSE_PROJECT_NAME;
|
|
13052
13609
|
return Object.keys(prev).length > 0 ? "compose" : "openship";
|
|
13053
13610
|
}
|
|
13611
|
+
var PGDATA_ROOT = "/var/lib/postgresql/data";
|
|
13612
|
+
function resolvePgData(prev) {
|
|
13613
|
+
if (prev.OPENSHIP_PGDATA) return prev.OPENSHIP_PGDATA;
|
|
13614
|
+
return dbVolumeExists(composeProjectName(prev)) ? PGDATA_ROOT : `${PGDATA_ROOT}/pgdata`;
|
|
13615
|
+
}
|
|
13054
13616
|
function orphanedStackContainers(project) {
|
|
13055
13617
|
const r2 = spawnSync3(
|
|
13056
13618
|
"docker",
|
|
@@ -13150,7 +13712,7 @@ function migrateLegacyEdgeVolumes(project) {
|
|
|
13150
13712
|
const existing = new Set(ls.stdout.split("\n").map((l2) => l2.trim()).filter(Boolean));
|
|
13151
13713
|
for (const { volume, host } of pairs) {
|
|
13152
13714
|
if (!existing.has(volume)) continue;
|
|
13153
|
-
|
|
13715
|
+
mkdirSync3(host, { recursive: true });
|
|
13154
13716
|
const alreadyThere = spawnSync3("sh", ["-c", `ls -A ${JSON.stringify(host)} 2>/dev/null | head -1`], {
|
|
13155
13717
|
encoding: "utf8"
|
|
13156
13718
|
});
|
|
@@ -13200,11 +13762,30 @@ function keepConfig(prev, key, explicit) {
|
|
|
13200
13762
|
const carried = prev[key]?.trim();
|
|
13201
13763
|
return carried || void 0;
|
|
13202
13764
|
}
|
|
13765
|
+
var ACME_ENV_KEYS = [
|
|
13766
|
+
"OPENSHIP_ACME_EMAIL",
|
|
13767
|
+
"OPENSHIP_ACME_DIRECTORY_URL",
|
|
13768
|
+
"OPENSHIP_ACME_EAB_KID",
|
|
13769
|
+
"OPENSHIP_ACME_EAB_HMAC_KEY",
|
|
13770
|
+
"OPENSHIP_ACME_KEY_TYPE",
|
|
13771
|
+
"OPENSHIP_ACME_CA_BUNDLE",
|
|
13772
|
+
"OPENSHIP_ACME_TOS_AGREED"
|
|
13773
|
+
];
|
|
13774
|
+
function renderAcmeEnv(prev) {
|
|
13775
|
+
return ACME_ENV_KEYS.flatMap((key) => {
|
|
13776
|
+
const value = process.env[key]?.trim() || prev[key]?.trim();
|
|
13777
|
+
if (!value) return [];
|
|
13778
|
+
if (/[\r\n]/.test(value)) throw new Error(`${key} must be a single-line value`);
|
|
13779
|
+
return [`${key}=${value}`];
|
|
13780
|
+
});
|
|
13781
|
+
}
|
|
13203
13782
|
function resolveEnvConfig(prev, opts) {
|
|
13204
13783
|
const publicUrl = keepConfig(prev, "OPENSHIP_PUBLIC_URL", opts.publicUrl);
|
|
13784
|
+
const bindAddr = keepConfig(prev, "OPENSHIP_BIND_ADDR") ?? (publicUrl ? "0.0.0.0" : void 0);
|
|
13205
13785
|
return {
|
|
13206
|
-
apiPort: keepConfig(prev, "API_PORT", opts.apiPort) ??
|
|
13207
|
-
dashPort: keepConfig(prev, "DASHBOARD_PORT", opts.dashboardPort) ??
|
|
13786
|
+
apiPort: keepConfig(prev, "API_PORT", opts.apiPort) ?? String(DEFAULT_API_PORT),
|
|
13787
|
+
dashPort: keepConfig(prev, "DASHBOARD_PORT", opts.dashboardPort) ?? String(DEFAULT_DASHBOARD_PORT),
|
|
13788
|
+
...bindAddr ? { bindAddr } : {},
|
|
13208
13789
|
...publicUrl ? { publicUrl } : {},
|
|
13209
13790
|
// A public URL always implies a proxy in front; otherwise keep whatever the
|
|
13210
13791
|
// install was configured with.
|
|
@@ -13222,6 +13803,50 @@ function resolveEnvConfig(prev, opts) {
|
|
|
13222
13803
|
hostControl: opts.noHostControl === void 0 ? prev.OPENSHIP_HOST_CONTROL !== "false" : !opts.noHostControl
|
|
13223
13804
|
};
|
|
13224
13805
|
}
|
|
13806
|
+
function toPort(value) {
|
|
13807
|
+
const n2 = Number(value);
|
|
13808
|
+
return Number.isInteger(n2) && n2 > 0 && n2 <= 65535 ? n2 : void 0;
|
|
13809
|
+
}
|
|
13810
|
+
function composeEnvPorts() {
|
|
13811
|
+
const env = readEnvFile();
|
|
13812
|
+
return { api: toPort(env.API_PORT), dashboard: toPort(env.DASHBOARD_PORT) };
|
|
13813
|
+
}
|
|
13814
|
+
function composeBindAddr() {
|
|
13815
|
+
return readEnvFile().OPENSHIP_BIND_ADDR?.trim() || "127.0.0.1";
|
|
13816
|
+
}
|
|
13817
|
+
function composeTrustedOriginUrls() {
|
|
13818
|
+
const env = readEnvFile();
|
|
13819
|
+
return [env.OPENSHIP_PUBLIC_URL, env.OPENSHIP_EXTRA_TRUSTED_ORIGINS].filter(
|
|
13820
|
+
(v2) => !!v2?.trim()
|
|
13821
|
+
);
|
|
13822
|
+
}
|
|
13823
|
+
function composeHeldPorts() {
|
|
13824
|
+
const r2 = spawnSync3(
|
|
13825
|
+
"docker",
|
|
13826
|
+
["ps", "--format", '{{.Label "com.docker.compose.project.config_files"}} {{.Ports}}'],
|
|
13827
|
+
{ encoding: "utf8" }
|
|
13828
|
+
);
|
|
13829
|
+
if (r2.status !== 0 || !r2.stdout) return [];
|
|
13830
|
+
const ports = /* @__PURE__ */ new Set();
|
|
13831
|
+
for (const line of r2.stdout.split("\n")) {
|
|
13832
|
+
const [configFiles = "", published = ""] = line.split(" ");
|
|
13833
|
+
if (!configFiles.split(",").some((f2) => f2.trim() === COMPOSE_FILE)) continue;
|
|
13834
|
+
for (const m2 of published.matchAll(/:(\d+)->/g)) ports.add(Number(m2[1]));
|
|
13835
|
+
}
|
|
13836
|
+
return [...ports];
|
|
13837
|
+
}
|
|
13838
|
+
async function resolveComposePorts(prefs, opts = {}) {
|
|
13839
|
+
return resolvePorts(
|
|
13840
|
+
{
|
|
13841
|
+
api: toPort(prefs.api),
|
|
13842
|
+
dashboard: toPort(prefs.dashboard),
|
|
13843
|
+
previous: composeEnvPorts(),
|
|
13844
|
+
bindAddr: composeBindAddr(),
|
|
13845
|
+
reclaimable: composeHeldPorts()
|
|
13846
|
+
},
|
|
13847
|
+
opts
|
|
13848
|
+
);
|
|
13849
|
+
}
|
|
13225
13850
|
function renderEnv(opts, host, cfg) {
|
|
13226
13851
|
const prev = readEnvFile();
|
|
13227
13852
|
const lines = [
|
|
@@ -13235,13 +13860,24 @@ function renderEnv(opts, host, cfg) {
|
|
|
13235
13860
|
// (hides the local row). Written explicitly so the policy is visible in .env.
|
|
13236
13861
|
`OPENSHIP_HOST_CONTROL=${cfg.hostControl ? "true" : "false"}`,
|
|
13237
13862
|
`OPENSHIP_IMAGE_REGISTRY=${cfg.registry}`,
|
|
13238
|
-
`OPENSHIP_VERSION=${opts.version || (true ? "0.
|
|
13863
|
+
`OPENSHIP_VERSION=${opts.version || (true ? "0.6.0" : "latest")}`,
|
|
13239
13864
|
`POSTGRES_PASSWORD=${keepSecret(prev, "POSTGRES_PASSWORD")}`,
|
|
13865
|
+
// Pinned once (see resolvePgData): fresh install → subdir, existing volume → root.
|
|
13866
|
+
`OPENSHIP_PGDATA=${resolvePgData(prev)}`,
|
|
13240
13867
|
`BETTER_AUTH_SECRET=${keepSecret(prev, "BETTER_AUTH_SECRET")}`,
|
|
13241
13868
|
`INTERNAL_TOKEN=${keepSecret(prev, "INTERNAL_TOKEN")}`,
|
|
13242
13869
|
`API_PORT=${cfg.apiPort}`,
|
|
13243
|
-
`DASHBOARD_PORT=${cfg.dashPort}
|
|
13870
|
+
`DASHBOARD_PORT=${cfg.dashPort}`,
|
|
13871
|
+
// The api's OWN view of the dashboard port: the self-app boot reconcile points
|
|
13872
|
+
// the operator's domain at the dashboard through this (self-deploy.ts), and it
|
|
13873
|
+
// silently defaults to 3001 when unset. Ports are dynamic now, so leaving it
|
|
13874
|
+
// out publishes a domain routed to a port nothing is listening on.
|
|
13875
|
+
`OPENSHIP_DASHBOARD_PORT=${cfg.dashPort}`,
|
|
13876
|
+
// Alternate CA/EAB values are operator configuration (including one secret),
|
|
13877
|
+
// so a routine `openship up`/upgrade must not silently discard them.
|
|
13878
|
+
...renderAcmeEnv(prev)
|
|
13244
13879
|
];
|
|
13880
|
+
if (cfg.bindAddr) lines.push(`OPENSHIP_BIND_ADDR=${cfg.bindAddr}`);
|
|
13245
13881
|
if (cfg.publicUrl) lines.push(`OPENSHIP_PUBLIC_URL=${cfg.publicUrl}`);
|
|
13246
13882
|
if (cfg.extraTrustedOrigins) {
|
|
13247
13883
|
lines.push(`OPENSHIP_EXTRA_TRUSTED_ORIGINS=${cfg.extraTrustedOrigins}`);
|
|
@@ -13261,7 +13897,7 @@ function renderEnv(opts, host, cfg) {
|
|
|
13261
13897
|
function sourceBuildDir() {
|
|
13262
13898
|
const marker = readSourceInstall();
|
|
13263
13899
|
if (!marker?.dir) return null;
|
|
13264
|
-
const hasAll = BUILT_SERVICES.every((s2) =>
|
|
13900
|
+
const hasAll = BUILT_SERVICES.every((s2) => existsSync4(join7(marker.dir, s2.dockerfile)));
|
|
13265
13901
|
return hasAll ? marker.dir : null;
|
|
13266
13902
|
}
|
|
13267
13903
|
function renderBuildOverride(repoDir) {
|
|
@@ -13277,22 +13913,56 @@ services:
|
|
|
13277
13913
|
${services}`;
|
|
13278
13914
|
}
|
|
13279
13915
|
function materialize(opts) {
|
|
13280
|
-
|
|
13916
|
+
mkdirSync3(COMPOSE_DIR, { recursive: true, mode: 448 });
|
|
13281
13917
|
const prev = readEnvFile();
|
|
13282
13918
|
const cfg = resolveEnvConfig(prev, opts);
|
|
13283
|
-
const host = cfg.hostControl
|
|
13919
|
+
const host = provisionHostSshChannel(cfg.hostControl);
|
|
13284
13920
|
let before = "";
|
|
13285
13921
|
try {
|
|
13286
|
-
before =
|
|
13922
|
+
before = readFileSync3(ENV_FILE, "utf8");
|
|
13287
13923
|
} catch {
|
|
13288
13924
|
}
|
|
13289
13925
|
const rendered = renderEnv(opts, host, cfg);
|
|
13290
|
-
|
|
13291
|
-
|
|
13926
|
+
writeFileSync2(COMPOSE_FILE, COMPOSE_YAML);
|
|
13927
|
+
writeFileSync2(ENV_FILE, rendered, { mode: 384 });
|
|
13292
13928
|
const buildDir = opts.build === false ? null : sourceBuildDir();
|
|
13293
|
-
if (buildDir)
|
|
13929
|
+
if (buildDir) writeFileSync2(BUILD_FILE, renderBuildOverride(buildDir));
|
|
13294
13930
|
return { buildDir, cfg, envChanged: before !== "" && rendered !== before };
|
|
13295
13931
|
}
|
|
13932
|
+
var SECRET_ENV_KEY = /(PASSWORD|SECRET|TOKEN|HMAC_KEY)$/;
|
|
13933
|
+
function composePlan(opts) {
|
|
13934
|
+
const prev = readEnvFile();
|
|
13935
|
+
const cfg = resolveEnvConfig(prev, opts);
|
|
13936
|
+
const buildDir = opts.build === false ? null : sourceBuildDir();
|
|
13937
|
+
const hostChannel = plannedHostChannel(cfg.hostControl);
|
|
13938
|
+
const settings = [];
|
|
13939
|
+
const newSecrets = [];
|
|
13940
|
+
for (const line of renderEnv(opts, hostChannel, cfg).split("\n")) {
|
|
13941
|
+
const at2 = line.indexOf("=");
|
|
13942
|
+
if (at2 < 1 || line.startsWith("#")) continue;
|
|
13943
|
+
const key = line.slice(0, at2);
|
|
13944
|
+
if (!SECRET_ENV_KEY.test(key)) {
|
|
13945
|
+
settings.push({ key, value: line.slice(at2 + 1) });
|
|
13946
|
+
continue;
|
|
13947
|
+
}
|
|
13948
|
+
const minted = !prev[key];
|
|
13949
|
+
if (minted) newSecrets.push(key);
|
|
13950
|
+
settings.push({ key, value: minted ? "<generated on this run>" : "<preserved>" });
|
|
13951
|
+
}
|
|
13952
|
+
return {
|
|
13953
|
+
composeFile: COMPOSE_FILE,
|
|
13954
|
+
envFile: ENV_FILE,
|
|
13955
|
+
installMethodFile: INSTALL_METHOD_FILE,
|
|
13956
|
+
...buildDir ? { buildFile: BUILD_FILE } : {},
|
|
13957
|
+
yaml: COMPOSE_YAML,
|
|
13958
|
+
settings,
|
|
13959
|
+
newSecrets,
|
|
13960
|
+
existing: Object.keys(prev).length > 0,
|
|
13961
|
+
mountDirs: EDGE_CONTAINER_MOUNTS.map((m2) => m2.host),
|
|
13962
|
+
buildDir,
|
|
13963
|
+
hostChannel
|
|
13964
|
+
};
|
|
13965
|
+
}
|
|
13296
13966
|
function dbVolumeExists(project) {
|
|
13297
13967
|
const r2 = spawnSync3("docker", ["volume", "inspect", `${project}_postgres_data`], {
|
|
13298
13968
|
stdio: "ignore"
|
|
@@ -13315,6 +13985,23 @@ function composePrefetch(opts) {
|
|
|
13315
13985
|
}
|
|
13316
13986
|
return compose(["pull"]) === 0;
|
|
13317
13987
|
}
|
|
13988
|
+
function ensureEdgeMountDirs() {
|
|
13989
|
+
const failed = [];
|
|
13990
|
+
for (const { host } of EDGE_CONTAINER_MOUNTS) {
|
|
13991
|
+
try {
|
|
13992
|
+
mkdirSync3(host, { recursive: true });
|
|
13993
|
+
} catch (err) {
|
|
13994
|
+
if (err.code !== "EEXIST") failed.push(host);
|
|
13995
|
+
}
|
|
13996
|
+
}
|
|
13997
|
+
return failed;
|
|
13998
|
+
}
|
|
13999
|
+
function isRootlessDocker() {
|
|
14000
|
+
const r2 = spawnSync3("docker", ["info", "-f", "{{println .SecurityOptions}}"], {
|
|
14001
|
+
encoding: "utf8"
|
|
14002
|
+
});
|
|
14003
|
+
return r2.status === 0 && /rootless/i.test(r2.stdout ?? "");
|
|
14004
|
+
}
|
|
13318
14005
|
async function composeUp(opts) {
|
|
13319
14006
|
const { buildDir, cfg, envChanged } = materialize(opts);
|
|
13320
14007
|
const apiPort = cfg.apiPort;
|
|
@@ -13328,6 +14015,21 @@ async function composeUp(opts) {
|
|
|
13328
14015
|
removeOrphanedStack(project);
|
|
13329
14016
|
migrateLegacyEdgeVolumes(project);
|
|
13330
14017
|
warnOrphanedVolumes(project);
|
|
14018
|
+
const unmakeableMounts = ensureEdgeMountDirs();
|
|
14019
|
+
if (unmakeableMounts.length && isRootlessDocker()) {
|
|
14020
|
+
const dirs = unmakeableMounts.join(" ");
|
|
14021
|
+
console.error(
|
|
14022
|
+
`
|
|
14023
|
+
Rootless Docker can't create the edge's host directories under root-owned paths:
|
|
14024
|
+
${unmakeableMounts.join("\n ")}
|
|
14025
|
+
|
|
14026
|
+
Create them once (owned by your user), then re-run \`openship up --compose\`:
|
|
14027
|
+
sudo mkdir -p ${dirs}
|
|
14028
|
+
sudo chown -R "$(id -un)" ${dirs}
|
|
14029
|
+
`
|
|
14030
|
+
);
|
|
14031
|
+
return { ok: false, apiPort, dashPort };
|
|
14032
|
+
}
|
|
13331
14033
|
if (dbVolumeExists(project)) {
|
|
13332
14034
|
reconcileDbPassword(env.POSTGRES_USER || "openship", env.POSTGRES_PASSWORD ?? "");
|
|
13333
14035
|
}
|
|
@@ -13365,12 +14067,12 @@ function onEdgeContainerChanged() {
|
|
|
13365
14067
|
invalidateEdgeContainer();
|
|
13366
14068
|
}
|
|
13367
14069
|
function composeDown() {
|
|
13368
|
-
if (!
|
|
14070
|
+
if (!existsSync4(COMPOSE_FILE)) return false;
|
|
13369
14071
|
return compose(["down"]) === 0;
|
|
13370
14072
|
}
|
|
13371
14073
|
function composeUninstall(opts = {}) {
|
|
13372
14074
|
const removedImages = [];
|
|
13373
|
-
const ok =
|
|
14075
|
+
const ok = existsSync4(COMPOSE_FILE) ? compose(["down", "-v", "--remove-orphans"]) === 0 : false;
|
|
13374
14076
|
if (opts.removeImages) {
|
|
13375
14077
|
const env = readEnvFile();
|
|
13376
14078
|
const registry = env.OPENSHIP_IMAGE_REGISTRY || DEFAULT_IMAGE_REGISTRY;
|
|
@@ -13385,7 +14087,7 @@ function composeUninstall(opts = {}) {
|
|
|
13385
14087
|
return { ok, removedImages };
|
|
13386
14088
|
}
|
|
13387
14089
|
async function composeUpdate(version) {
|
|
13388
|
-
if (!
|
|
14090
|
+
if (!existsSync4(COMPOSE_FILE)) return false;
|
|
13389
14091
|
return (await composeUp(version ? { version } : {})).ok;
|
|
13390
14092
|
}
|
|
13391
14093
|
function composePs() {
|
|
@@ -13421,7 +14123,16 @@ async function waitForApiHealth(port, tries) {
|
|
|
13421
14123
|
}
|
|
13422
14124
|
return false;
|
|
13423
14125
|
}
|
|
13424
|
-
async function
|
|
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) {
|
|
13425
14136
|
const token = composeInternalToken();
|
|
13426
14137
|
if (!token) {
|
|
13427
14138
|
const error = "couldn't read the stack's internal token";
|
|
@@ -13437,11 +14148,16 @@ async function importMigratedSites(apiPort, sites, certPems) {
|
|
|
13437
14148
|
spinner.warn(`${error} \u2014 migrated sites not imported. Re-run \`openship up\` to retry.`);
|
|
13438
14149
|
return { ok: false, registered: [], error };
|
|
13439
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
|
+
}
|
|
13440
14156
|
try {
|
|
13441
14157
|
const r2 = await fetch(`http://127.0.0.1:${apiPort}/api/system/edge/import-sites`, {
|
|
13442
14158
|
method: "POST",
|
|
13443
14159
|
headers: { "Content-Type": "application/json", "X-Internal-Token": token },
|
|
13444
|
-
body: JSON.stringify({ sites, certPems }),
|
|
14160
|
+
body: JSON.stringify({ sites, certPems, staticRootOverrides }),
|
|
13445
14161
|
signal: AbortSignal.timeout(12e4)
|
|
13446
14162
|
});
|
|
13447
14163
|
const data = await r2.json().catch(() => ({}));
|
|
@@ -13496,16 +14212,36 @@ async function importMigratedSites(apiPort, sites, certPems) {
|
|
|
13496
14212
|
export {
|
|
13497
14213
|
OS_DIR,
|
|
13498
14214
|
IS_ALT_HOME,
|
|
14215
|
+
DATA_DIR,
|
|
14216
|
+
LOG_DIR,
|
|
14217
|
+
AUTH_SECRET_FILE,
|
|
14218
|
+
INTERNAL_TOKEN_FILE,
|
|
14219
|
+
PORTS_FILE,
|
|
14220
|
+
saveInstanceUrl,
|
|
14221
|
+
readInstanceUrl,
|
|
14222
|
+
readStoredPorts,
|
|
14223
|
+
storedApiPort,
|
|
14224
|
+
storedDashboardPort,
|
|
14225
|
+
portMoveNotice,
|
|
14226
|
+
resolvePorts,
|
|
14227
|
+
DEFAULT_REPO,
|
|
14228
|
+
SOURCE_CHECKOUT_DIR,
|
|
14229
|
+
SOURCE_DIST_DIR,
|
|
14230
|
+
has,
|
|
13499
14231
|
shortSha,
|
|
13500
14232
|
prepareFromSource,
|
|
13501
14233
|
readSourceInstall,
|
|
13502
14234
|
remoteSha,
|
|
13503
14235
|
rebuildFromSource,
|
|
13504
14236
|
readInstallMethod,
|
|
14237
|
+
dockerGap,
|
|
13505
14238
|
hasDockerCompose,
|
|
13506
|
-
|
|
14239
|
+
dockerInstallPreview,
|
|
13507
14240
|
ensureDocker,
|
|
14241
|
+
composeTrustedOriginUrls,
|
|
14242
|
+
resolveComposePorts,
|
|
13508
14243
|
sourceBuildDir,
|
|
14244
|
+
composePlan,
|
|
13509
14245
|
composePrefetch,
|
|
13510
14246
|
composeUp,
|
|
13511
14247
|
composeDown,
|
|
@@ -13517,5 +14253,6 @@ export {
|
|
|
13517
14253
|
composeRestart,
|
|
13518
14254
|
composeInternalToken,
|
|
13519
14255
|
waitForApiHealth,
|
|
14256
|
+
waitForEdgeRunning,
|
|
13520
14257
|
importMigratedSites
|
|
13521
14258
|
};
|