openship 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -1
- package/dist/bare-IWS2QHJ3.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-BS2AXYZX.js → chunk-2NZSUXOB.js} +647 -214
- 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-NNPEG6HM.js → chunk-3NZICQHF.js} +2 -2
- package/dist/{chunk-OWED2GRM.js → chunk-42WQABXM.js} +2 -2
- package/dist/{chunk-TYVME3XD.js → chunk-6FH4AXVY.js} +20 -3
- package/dist/{chunk-ZCOQJCZO.js → chunk-6ODUBKUB.js} +3 -3
- package/dist/{chunk-DYPH4HCI.js → chunk-AO252K7Z.js} +1 -1
- package/dist/{chunk-IXCHURVK.js → chunk-DOD2BXHQ.js} +4 -4
- package/dist/{chunk-VWPEKOH6.js → chunk-DRK3MEDS.js} +3 -3
- package/dist/{chunk-V7BZMGOT.js → chunk-EMXI4UCU.js} +5 -5
- package/dist/{chunk-RB35YN3J.js → chunk-EW6KB2J7.js} +391 -63
- package/dist/chunk-I3DPMTSF.js +29 -0
- package/dist/{chunk-NXFIRK2Z.js → chunk-IOT5ZX23.js} +573 -459
- package/dist/{chunk-KZOBYJUW.js → chunk-IW2UKBVF.js} +3 -3
- package/dist/{chunk-YWMODKWL.js → chunk-IXM5245F.js} +165 -13
- package/dist/{chunk-KNTJ7PSF.js → chunk-JIMUYEKN.js} +4 -4
- package/dist/{chunk-O6ZZF52V.js → chunk-ONKEYSMY.js} +2 -2
- package/dist/{chunk-UMQP7SYT.js → chunk-OTLSJYAC.js} +1316 -53
- package/dist/{chunk-EI575GHN.js → chunk-PDW4L3NV.js} +3 -3
- package/dist/{chunk-KI2EY3WB.js → chunk-PXKCZB2I.js} +1 -1
- package/dist/{chunk-6APIPIJY.js → chunk-QWHBRY2K.js} +5 -5
- package/dist/{chunk-JLKKYUZA.js → chunk-RRJJGVKE.js} +7 -7
- package/dist/{chunk-6B5YCFFV.js → chunk-RVER2EOO.js} +5 -5
- package/dist/{chunk-Q2CIRAOK.js → chunk-RXQBD4ZY.js} +1 -1
- package/dist/{chunk-6LKHXB6T.js → chunk-TWJCLD4W.js} +48 -13
- package/dist/{chunk-K2RPVGPN.js → chunk-UC7YIEHT.js} +36 -17
- package/dist/{chunk-I6PSTQIR.js → chunk-VDDMADOF.js} +3 -3
- package/dist/{chunk-6PIYRG5I.js → chunk-VZ2ZHPS2.js} +52 -42
- package/dist/{chunk-AVIB7CT7.js → chunk-WNWQBB7F.js} +2 -2
- package/dist/{chunk-FRNAR33O.js → chunk-XZYFZ6A5.js} +3 -3
- package/dist/chunk-YIPMDGG6.js +703 -0
- 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-7QYOGF7F.js → cloud-KRTZ2IL5.js} +3 -3
- package/dist/cloud-M2YLWEXO.js +31 -0
- package/dist/{detect-BAJMY6KM.js → detect-ALF4GXRX.js} +7 -8
- package/dist/{dist-IQJ6H6UZ.js → dist-BM6NPWIH.js} +3 -3
- package/dist/{dist-es-X3B5QYKB.js → dist-es-2YRDXMUU.js} +6 -6
- package/dist/{dist-es-OSNJBLXB.js → dist-es-CCVZEFRZ.js} +5 -5
- package/dist/{dist-es-VNKHQVIB.js → dist-es-IOH6G4GM.js} +4 -4
- package/dist/{dist-es-RYHIPOTN.js → dist-es-JGBPABJI.js} +14 -14
- package/dist/{dist-es-JSTDUS7Q.js → dist-es-KWSMDPIP.js} +6 -6
- package/dist/{dist-es-5DY7K32L.js → dist-es-UYDDOVZ3.js} +6 -6
- package/dist/{dist-es-WV4AMOII.js → dist-es-XUOR4NQN.js} +8 -8
- package/dist/docker-QECQJ5P4.js +32 -0
- package/dist/docker-edge-executor-62N64MGS.js +17 -0
- package/dist/edge-import-SAYTRJLO.js +48 -0
- package/dist/ensure-container-edge-CFSCPGZK.js +32 -0
- package/dist/{event-streams-EHRAKL75.js → event-streams-TR4GAC53.js} +4 -4
- package/dist/{executor-LOYMPU67.js → executor-PT6UEJ5M.js} +8 -8
- package/dist/index.js +1228 -463
- 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-YOEEFBTE.js +31 -0
- package/dist/node-entry.js +4 -0
- package/dist/{noop-FMMFG2EZ.js → noop-NJ72F46T.js} +3 -3
- package/dist/openresty-lua-WGBO2M3Z.js +81 -0
- package/dist/server/index.js +23276 -8015
- package/dist/server/lua/geo_country.lua +34 -8
- package/dist/server/lua/maxminddb.lua +403 -0
- package/dist/server/lua/mgmt_api.lua +431 -68
- package/dist/server/lua/pipe_stream.lua +52 -39
- package/dist/server/lua/site_logger.lua +236 -30
- package/dist/server/migrations/0082_edge_target_verification.sql +39 -0
- package/dist/server/migrations/0083_service_incident.sql +84 -0
- package/dist/server/migrations/0084_analytics_daily_rollup.sql +72 -0
- package/dist/server/migrations/0085_resource_usage.sql +68 -0
- package/dist/server/migrations/0086_remote_infra_updates.sql +41 -0
- package/dist/server/migrations/0087_server_container_version_columns.sql +17 -0
- package/dist/server/migrations/0088_audit_source_and_settings.sql +27 -0
- package/dist/server/migrations/0089_auto_scan_infra.sql +7 -0
- package/dist/server/migrations/0090_project_collect_paths.sql +18 -0
- package/dist/server/migrations/0091_project_server_id.sql +26 -0
- package/dist/server/migrations/0092_project_internal_alias.sql +13 -0
- package/dist/server/migrations/0093_service_incident_org_index.sql +13 -0
- package/dist/server/migrations/0094_backup_restore_meta.sql +15 -0
- package/dist/server/migrations/0095_backup_restore_cancel.sql +19 -0
- package/dist/server/migrations/0096_backup_policy_retention_defaults.sql +20 -0
- package/dist/server/migrations/0097_update_status_upstream_only.sql +35 -0
- package/dist/server/migrations/meta/_journal.json +112 -0
- package/dist/setup-M2ZDGH3O.js +21 -0
- package/dist/{signin-ITOPKKUB.js → signin-3M3UPB6K.js} +10 -10
- package/dist/{sso-oidc-NON2PD7Y.js → sso-oidc-3VDIYOEJ.js} +10 -10
- package/dist/{sts-HHFFSAY2.js → sts-B3WIYLXR.js} +9 -9
- package/package.json +6 -3
- package/dist/bare-VRGE5MXH.js +0 -23
- package/dist/chunk-CUJXGK7R.js +0 -3167
- package/dist/chunk-EAAGSQXN.js +0 -377
- package/dist/cloud-3M2WZ624.js +0 -30
- package/dist/docker-2NGBRJFJ.js +0 -18
- package/dist/docker-edge-executor-5I4M5QT7.js +0 -13
- package/dist/edge-import-RIWNEIXQ.js +0 -47
- package/dist/ensure-container-edge-3NSYKHRX.js +0 -31
- package/dist/nginx-QE4DOM5J.js +0 -24
- package/dist/openresty-lua-BTAKECCF.js +0 -42
- package/dist/server/lua/pipe_log.lua +0 -71
- package/dist/setup-KELALK7H.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-RRJJGVKE.js";
|
|
105
105
|
import {
|
|
106
106
|
systemCatalog
|
|
107
|
-
} from "./chunk-
|
|
107
|
+
} from "./chunk-IOT5ZX23.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,33 +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-YIPMDGG6.js";
|
|
159
164
|
import {
|
|
160
165
|
DEFAULT_IMAGE_REGISTRY,
|
|
161
166
|
isAwsS3Endpoint,
|
|
162
|
-
safeErrorMessage
|
|
163
|
-
|
|
167
|
+
safeErrorMessage,
|
|
168
|
+
withTimeout
|
|
169
|
+
} from "./chunk-OTLSJYAC.js";
|
|
164
170
|
|
|
165
171
|
// src/lib/edge-import.ts
|
|
166
172
|
import chalk from "chalk";
|
|
@@ -169,8 +175,42 @@ import ora from "ora";
|
|
|
169
175
|
// ../../packages/adapters/src/runtime/image-transfer.ts
|
|
170
176
|
import { Transform } from "stream";
|
|
171
177
|
|
|
172
|
-
// ../../packages/adapters/src/
|
|
173
|
-
|
|
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`;
|
|
174
214
|
|
|
175
215
|
// ../../packages/adapters/src/system/modules/verify.ts
|
|
176
216
|
import { createHash, createPublicKey, verify as cryptoVerify } from "crypto";
|
|
@@ -244,6 +284,25 @@ function decryptCredential(stored) {
|
|
|
244
284
|
import { createHash as createHash3 } from "crypto";
|
|
245
285
|
import { Transform as Transform2 } from "stream";
|
|
246
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
|
+
|
|
247
306
|
// ../../packages/adapters/src/backup/volume-transfer.ts
|
|
248
307
|
import { Transform as Transform3 } from "stream";
|
|
249
308
|
|
|
@@ -486,8 +545,7 @@ var CustomCommandProducerImpl = class {
|
|
|
486
545
|
kind = "custom_command";
|
|
487
546
|
// No detect — explicit selection only.
|
|
488
547
|
async *produce(service, executor, opts) {
|
|
489
|
-
const
|
|
490
|
-
const produceCommand = cfg.produceCommand ?? opts.command;
|
|
548
|
+
const produceCommand = opts.produceCommand ?? opts.command;
|
|
491
549
|
if (!produceCommand) {
|
|
492
550
|
throw new Error("custom_command producer requires `produceCommand` in policy payload config");
|
|
493
551
|
}
|
|
@@ -497,12 +555,15 @@ var CustomCommandProducerImpl = class {
|
|
|
497
555
|
produceCommand
|
|
498
556
|
]);
|
|
499
557
|
yield {
|
|
500
|
-
name:
|
|
558
|
+
name: opts.artifactName ?? "custom-backup.bin",
|
|
501
559
|
stream: stdout,
|
|
502
560
|
payloadKind: "custom_command",
|
|
503
561
|
metadata: {
|
|
504
562
|
produceCommand,
|
|
505
|
-
|
|
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
|
|
506
567
|
}
|
|
507
568
|
};
|
|
508
569
|
const exit = await awaitExit;
|
|
@@ -516,7 +577,7 @@ var CustomCommandProducerImpl = class {
|
|
|
516
577
|
const restoreCommand = artifact.metadata.restoreCommand ?? null;
|
|
517
578
|
if (!restoreCommand) {
|
|
518
579
|
throw new Error(
|
|
519
|
-
"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."
|
|
520
581
|
);
|
|
521
582
|
}
|
|
522
583
|
const body = await artifact.open();
|
|
@@ -583,7 +644,8 @@ var VolumeCopyProducerImpl = class {
|
|
|
583
644
|
const stream = await artifact.open();
|
|
584
645
|
await executor.receiveStream(service, volumeId, stream, {
|
|
585
646
|
compression: artifact.metadata.compression ?? "zstd",
|
|
586
|
-
clearTarget: opts.clearTarget ?? true
|
|
647
|
+
clearTarget: opts.clearTarget ?? true,
|
|
648
|
+
signal: opts.signal
|
|
587
649
|
});
|
|
588
650
|
}
|
|
589
651
|
};
|
|
@@ -596,6 +658,58 @@ registerProducer(VolumeCopyProducer);
|
|
|
596
658
|
// ../../packages/adapters/src/backup/executors/docker.ts
|
|
597
659
|
import { PassThrough } from "stream";
|
|
598
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
|
+
}
|
|
599
713
|
function shellEscape4(s2) {
|
|
600
714
|
return `'${s2.replace(/'/g, "'\\''")}'`;
|
|
601
715
|
}
|
|
@@ -669,12 +783,12 @@ var DockerBackupExecutor = class {
|
|
|
669
783
|
WorkingDir: opts?.cwd,
|
|
670
784
|
Env: opts?.env ? Object.entries(opts.env).map(([k2, v2]) => `${k2}=${v2}`) : void 0
|
|
671
785
|
});
|
|
672
|
-
const stream = await exec.start({
|
|
786
|
+
const stream = await exec.start({ stdin: false });
|
|
673
787
|
return this.attachDemuxed(this.dockerode, exec.id, stream, opts?.timeoutMs);
|
|
674
788
|
}
|
|
675
789
|
async streamPath(service, sourceId, opts) {
|
|
676
790
|
const sources = await this.listSources(service);
|
|
677
|
-
const source = sources
|
|
791
|
+
const source = matchBackupSource(sources, sourceId);
|
|
678
792
|
if (!source) {
|
|
679
793
|
throw new Error(`Backup source "${sourceId}" not found on service ${service.name}`);
|
|
680
794
|
}
|
|
@@ -692,7 +806,12 @@ var DockerBackupExecutor = class {
|
|
|
692
806
|
await this.ensureImage(helperImage);
|
|
693
807
|
const hostConfig = {
|
|
694
808
|
Binds: [`${source.source}:/mnt:ro`],
|
|
695
|
-
|
|
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,
|
|
696
815
|
// The archive streams over `attach()` below — the daemon's default
|
|
697
816
|
// json-file driver would ALSO write every byte of it to
|
|
698
817
|
// /var/lib/docker/containers as a log. That's a second copy of the whole
|
|
@@ -702,29 +821,37 @@ var DockerBackupExecutor = class {
|
|
|
702
821
|
// the helper exits. Nothing ever reads these logs.
|
|
703
822
|
LogConfig: { Type: "none", Config: {} }
|
|
704
823
|
};
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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
|
+
);
|
|
724
850
|
}
|
|
725
851
|
async receiveStream(service, targetSourceId, body, opts) {
|
|
852
|
+
opts?.signal?.throwIfAborted();
|
|
726
853
|
const sources = await this.listSources(service);
|
|
727
|
-
const source = sources
|
|
854
|
+
const source = matchBackupSource(sources, targetSourceId);
|
|
728
855
|
if (!source) {
|
|
729
856
|
throw new Error(`Restore target "${targetSourceId}" not found on service ${service.name}`);
|
|
730
857
|
}
|
|
@@ -736,49 +863,188 @@ var DockerBackupExecutor = class {
|
|
|
736
863
|
await this.ensureImage(helperImage);
|
|
737
864
|
const clearCmd = opts?.clearTarget ? `find /mnt -mindepth 1 -delete 2>/dev/null || true; ` : "";
|
|
738
865
|
const untarCmd = compression === "zstd" ? `${clearCmd}zstd -d -c | tar -x -C /mnt` : `${clearCmd}tar -x${compressionFlag(compression)}f - -C /mnt`;
|
|
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
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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}` : ""}`
|
|
779
1044
|
);
|
|
1045
|
+
} finally {
|
|
1046
|
+
polling = false;
|
|
780
1047
|
}
|
|
781
|
-
return { bytesWritten };
|
|
782
1048
|
}
|
|
783
1049
|
/**
|
|
784
1050
|
* SAME-DAEMON volume→volume copy in ONE helper container that mounts both
|
|
@@ -792,8 +1058,8 @@ var DockerBackupExecutor = class {
|
|
|
792
1058
|
* network-isolated; source is mounted read-only.
|
|
793
1059
|
*/
|
|
794
1060
|
async copyVolumeLocal(srcService, srcSourceId, dstService, dstSourceId, opts) {
|
|
795
|
-
const src = (await this.listSources(srcService)
|
|
796
|
-
const dst = (await this.listSources(dstService)
|
|
1061
|
+
const src = matchBackupSource(await this.listSources(srcService), srcSourceId);
|
|
1062
|
+
const dst = matchBackupSource(await this.listSources(dstService), dstSourceId);
|
|
797
1063
|
if (!src) throw new Error(`Copy source "${srcSourceId}" not found on ${srcService.name}`);
|
|
798
1064
|
if (!dst) throw new Error(`Copy target "${dstSourceId}" not found on ${dstService.name}`);
|
|
799
1065
|
if (src.type === "tmpfs" || dst.type === "tmpfs") {
|
|
@@ -802,31 +1068,30 @@ var DockerBackupExecutor = class {
|
|
|
802
1068
|
await this.ensureImage(HELPER_IMAGE);
|
|
803
1069
|
const clearCmd = opts?.clearTarget ? "find /to -mindepth 1 -delete 2>/dev/null || true; " : "";
|
|
804
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`;
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
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
|
|
810
1081
|
},
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
);
|
|
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 };
|
|
823
1093
|
}
|
|
824
|
-
|
|
825
|
-
return { bytesWritten: Number.isFinite(kb) ? kb * 1024 : 0 };
|
|
826
|
-
} finally {
|
|
827
|
-
await helper.remove({ force: true }).catch(() => {
|
|
828
|
-
});
|
|
829
|
-
}
|
|
1094
|
+
);
|
|
830
1095
|
}
|
|
831
1096
|
/**
|
|
832
1097
|
* Does the named volume behind `sourceId` already exist on this daemon, and
|
|
@@ -836,7 +1101,7 @@ var DockerBackupExecutor = class {
|
|
|
836
1101
|
* so we err on the side of NOT clobbering.
|
|
837
1102
|
*/
|
|
838
1103
|
async probeVolume(service, sourceId) {
|
|
839
|
-
const source = (await this.listSources(service)
|
|
1104
|
+
const source = matchBackupSource(await this.listSources(service), sourceId);
|
|
840
1105
|
if (!source || source.type !== "volume" || !source.source) {
|
|
841
1106
|
return { exists: false, empty: true };
|
|
842
1107
|
}
|
|
@@ -846,26 +1111,25 @@ var DockerBackupExecutor = class {
|
|
|
846
1111
|
return { exists: false, empty: true };
|
|
847
1112
|
}
|
|
848
1113
|
await this.ensureImage(HELPER_IMAGE);
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
}
|
|
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
|
+
);
|
|
869
1133
|
}
|
|
870
1134
|
async pipeIntoCommand(service, cmd, body, opts) {
|
|
871
1135
|
if (!service.containerId) {
|
|
@@ -883,7 +1147,10 @@ var DockerBackupExecutor = class {
|
|
|
883
1147
|
WorkingDir: opts?.cwd,
|
|
884
1148
|
Env: opts?.env ? Object.entries(opts.env).map(([k2, v2]) => `${k2}=${v2}`) : void 0
|
|
885
1149
|
});
|
|
886
|
-
const stream = await exec.
|
|
1150
|
+
const stream = await startExecStream(daemonConnectionFrom(this.dockerode), exec.id, {
|
|
1151
|
+
tty: false,
|
|
1152
|
+
stdin: true
|
|
1153
|
+
});
|
|
887
1154
|
const stderrChunks = [];
|
|
888
1155
|
const { PassThrough: PassThrough3 } = await import("stream");
|
|
889
1156
|
const stdoutSink = new PassThrough3();
|
|
@@ -915,9 +1182,8 @@ var DockerBackupExecutor = class {
|
|
|
915
1182
|
stream.on("end", async () => {
|
|
916
1183
|
if (timer) clearTimeout(timer);
|
|
917
1184
|
try {
|
|
918
|
-
const info = await this.dockerode.getExec(exec.id).inspect();
|
|
919
1185
|
resolve4({
|
|
920
|
-
code:
|
|
1186
|
+
code: await resolveExecExitCode(exec, exec.id),
|
|
921
1187
|
stderr: Buffer.concat(stderrChunks).toString("utf8").slice(0, 16 * 1024)
|
|
922
1188
|
});
|
|
923
1189
|
} catch (err) {
|
|
@@ -980,9 +1246,8 @@ var DockerBackupExecutor = class {
|
|
|
980
1246
|
stream.on("end", async () => {
|
|
981
1247
|
if (timer) clearTimeout(timer);
|
|
982
1248
|
try {
|
|
983
|
-
const info = await docker.getExec(execId).inspect();
|
|
984
1249
|
resolve4({
|
|
985
|
-
code:
|
|
1250
|
+
code: await resolveExecExitCode(docker.getExec(execId), execId),
|
|
986
1251
|
stderr: Buffer.concat(stderrChunks).toString("utf8").slice(0, 16 * 1024)
|
|
987
1252
|
});
|
|
988
1253
|
} catch (err) {
|
|
@@ -996,7 +1261,7 @@ var DockerBackupExecutor = class {
|
|
|
996
1261
|
});
|
|
997
1262
|
return { stdout, awaitExit };
|
|
998
1263
|
}
|
|
999
|
-
demuxContainerStream(container, stream,
|
|
1264
|
+
demuxContainerStream(container, stream, opts) {
|
|
1000
1265
|
const stdout = new PassThrough();
|
|
1001
1266
|
const stderrChunks = [];
|
|
1002
1267
|
const stderrSink = new PassThrough();
|
|
@@ -1004,29 +1269,49 @@ var DockerBackupExecutor = class {
|
|
|
1004
1269
|
if (stderrChunks.length < 16) stderrChunks.push(chunk);
|
|
1005
1270
|
});
|
|
1006
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
|
+
};
|
|
1007
1279
|
const endSinks = () => {
|
|
1008
1280
|
stdout.end();
|
|
1009
1281
|
stderrSink.end();
|
|
1282
|
+
sinksEnded = true;
|
|
1283
|
+
reapIfDone();
|
|
1010
1284
|
};
|
|
1011
1285
|
stream.on("end", endSinks);
|
|
1012
1286
|
stream.on("close", endSinks);
|
|
1013
|
-
const
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
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;
|
|
1020
1300
|
setTimeout(endSinks, 3e3);
|
|
1021
|
-
|
|
1301
|
+
reapIfDone();
|
|
1302
|
+
return {
|
|
1022
1303
|
code: res.StatusCode,
|
|
1023
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(() => {
|
|
1024
1309
|
});
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
}
|
|
1029
|
-
});
|
|
1310
|
+
throw err;
|
|
1311
|
+
} finally {
|
|
1312
|
+
watchdog.dispose();
|
|
1313
|
+
}
|
|
1314
|
+
})();
|
|
1030
1315
|
return { stdout, awaitExit };
|
|
1031
1316
|
}
|
|
1032
1317
|
};
|
|
@@ -1268,7 +1553,7 @@ var BareBackupExecutor = class {
|
|
|
1268
1553
|
]);
|
|
1269
1554
|
const path = sourceId;
|
|
1270
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(" ")} .`;
|
|
1271
|
-
return this.execStream(service, ["sh", "-c", tarCmd]);
|
|
1556
|
+
return this.execStream(service, ["sh", "-c", tarCmd], { timeoutMs: opts?.timeoutMs });
|
|
1272
1557
|
}
|
|
1273
1558
|
async receiveStream(service, targetSourceId, body, opts) {
|
|
1274
1559
|
const compression = opts?.compression ?? "zstd";
|
|
@@ -1379,7 +1664,7 @@ var LocalDestinationImpl = class {
|
|
|
1379
1664
|
return { ok: false, reason: message };
|
|
1380
1665
|
}
|
|
1381
1666
|
}
|
|
1382
|
-
async put(key, body,
|
|
1667
|
+
async put(key, body, opts) {
|
|
1383
1668
|
const targetPath = this.resolveKey(key);
|
|
1384
1669
|
await fs2.mkdir(dirname(targetPath), { recursive: true });
|
|
1385
1670
|
const tmpPath = `${targetPath}.tmp-${randomBytes3(4).toString("hex")}`;
|
|
@@ -1392,15 +1677,23 @@ var LocalDestinationImpl = class {
|
|
|
1392
1677
|
cb(null, chunk);
|
|
1393
1678
|
}
|
|
1394
1679
|
});
|
|
1680
|
+
let digest;
|
|
1395
1681
|
try {
|
|
1396
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
|
+
}
|
|
1397
1690
|
await fs2.rename(tmpPath, targetPath);
|
|
1398
1691
|
} catch (err) {
|
|
1399
1692
|
await fs2.unlink(tmpPath).catch(() => {
|
|
1400
1693
|
});
|
|
1401
1694
|
throw err;
|
|
1402
1695
|
}
|
|
1403
|
-
return { bytesWritten, etag:
|
|
1696
|
+
return { bytesWritten, etag: digest };
|
|
1404
1697
|
}
|
|
1405
1698
|
async get(key) {
|
|
1406
1699
|
const path = this.resolveKey(key);
|
|
@@ -10985,10 +11278,10 @@ var package_default = {
|
|
|
10985
11278
|
// ../../node_modules/@aws-sdk/credential-provider-node/dist-es/remoteProvider.js
|
|
10986
11279
|
var ENV_IMDS_DISABLED = "AWS_EC2_METADATA_DISABLED";
|
|
10987
11280
|
var remoteProvider = async (init) => {
|
|
10988
|
-
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");
|
|
10989
11282
|
if (process.env[ENV_CMDS_RELATIVE_URI] || process.env[ENV_CMDS_FULL_URI]) {
|
|
10990
11283
|
init.logger?.debug("@aws-sdk/credential-provider-node - remoteProvider::fromHttp/fromContainerMetadata");
|
|
10991
|
-
const { fromHttp } = await import("./dist-es-
|
|
11284
|
+
const { fromHttp } = await import("./dist-es-UYDDOVZ3.js");
|
|
10992
11285
|
return chain(fromHttp(init), fromContainerMetadata(init));
|
|
10993
11286
|
}
|
|
10994
11287
|
if (process.env[ENV_IMDS_DISABLED] && process.env[ENV_IMDS_DISABLED] !== "false") {
|
|
@@ -11100,22 +11393,22 @@ var defaultProvider = (init = {}) => memoizeChain([
|
|
|
11100
11393
|
if (!ssoStartUrl && !ssoAccountId && !ssoRegion && !ssoRoleName && !ssoSession) {
|
|
11101
11394
|
throw new CredentialsProviderError("Skipping SSO provider in default chain (inputs do not include SSO fields).", { logger: init.logger });
|
|
11102
11395
|
}
|
|
11103
|
-
const { fromSSO } = await import("./dist-es-
|
|
11396
|
+
const { fromSSO } = await import("./dist-es-XUOR4NQN.js");
|
|
11104
11397
|
return fromSSO(init)(awsIdentityProperties);
|
|
11105
11398
|
},
|
|
11106
11399
|
async (awsIdentityProperties) => {
|
|
11107
11400
|
init.logger?.debug("@aws-sdk/credential-provider-node - defaultProvider::fromIni");
|
|
11108
|
-
const { fromIni } = await import("./dist-es-
|
|
11401
|
+
const { fromIni } = await import("./dist-es-JGBPABJI.js");
|
|
11109
11402
|
return fromIni(init)(awsIdentityProperties);
|
|
11110
11403
|
},
|
|
11111
11404
|
async (awsIdentityProperties) => {
|
|
11112
11405
|
init.logger?.debug("@aws-sdk/credential-provider-node - defaultProvider::fromProcess");
|
|
11113
|
-
const { fromProcess } = await import("./dist-es-
|
|
11406
|
+
const { fromProcess } = await import("./dist-es-CCVZEFRZ.js");
|
|
11114
11407
|
return fromProcess(init)(awsIdentityProperties);
|
|
11115
11408
|
},
|
|
11116
11409
|
async (awsIdentityProperties) => {
|
|
11117
11410
|
init.logger?.debug("@aws-sdk/credential-provider-node - defaultProvider::fromTokenFile");
|
|
11118
|
-
const { fromTokenFile } = await import("./dist-es-
|
|
11411
|
+
const { fromTokenFile } = await import("./dist-es-2YRDXMUU.js");
|
|
11119
11412
|
return fromTokenFile(init)(awsIdentityProperties);
|
|
11120
11413
|
},
|
|
11121
11414
|
async () => {
|
|
@@ -12690,6 +12983,10 @@ var DEFAULT_HOME = join3(homedir(), ".openship");
|
|
|
12690
12983
|
var override = process.env.OPENSHIP_HOME?.trim();
|
|
12691
12984
|
var OS_DIR = override ? resolve2(override) : DEFAULT_HOME;
|
|
12692
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");
|
|
12693
12990
|
|
|
12694
12991
|
// src/lib/ports.ts
|
|
12695
12992
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -12851,13 +13148,13 @@ function stalePortOrigins(movedFrom, urls) {
|
|
|
12851
13148
|
}
|
|
12852
13149
|
return stale;
|
|
12853
13150
|
}
|
|
12854
|
-
async function resolvePorts(prefs) {
|
|
13151
|
+
async function resolvePorts(prefs, opts = {}) {
|
|
12855
13152
|
const resolved = await resolvePortPair({
|
|
12856
13153
|
...prefs,
|
|
12857
13154
|
stored: readStoredPorts(),
|
|
12858
13155
|
defaults: { api: DEFAULT_API_PORT, dashboard: DEFAULT_DASHBOARD_PORT }
|
|
12859
13156
|
});
|
|
12860
|
-
saveStoredPorts(resolved.api, resolved.dashboard);
|
|
13157
|
+
if (opts.persist !== false) saveStoredPorts(resolved.api, resolved.dashboard);
|
|
12861
13158
|
return resolved;
|
|
12862
13159
|
}
|
|
12863
13160
|
|
|
@@ -12871,6 +13168,8 @@ import { spawn, spawnSync } from "child_process";
|
|
|
12871
13168
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
12872
13169
|
import { join as join5, resolve as resolve3 } from "path";
|
|
12873
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");
|
|
12874
13173
|
function has(cmd) {
|
|
12875
13174
|
try {
|
|
12876
13175
|
return spawnSync(cmd, ["--version"], { stdio: "ignore" }).status === 0;
|
|
@@ -12923,7 +13222,7 @@ async function prepareFromSource(opts) {
|
|
|
12923
13222
|
}
|
|
12924
13223
|
ref = (opts.ref || "main").trim();
|
|
12925
13224
|
const repoUrl = opts.repo || DEFAULT_REPO;
|
|
12926
|
-
sourceDir =
|
|
13225
|
+
sourceDir = SOURCE_CHECKOUT_DIR;
|
|
12927
13226
|
mkdirSync2(OS_DIR, { recursive: true });
|
|
12928
13227
|
if (!existsSync2(join5(sourceDir, ".git"))) {
|
|
12929
13228
|
console.log(` Cloning ${repoUrl} \u2192 ${sourceDir}`);
|
|
@@ -12940,7 +13239,7 @@ async function prepareFromSource(opts) {
|
|
|
12940
13239
|
console.log(` Source: ${sourceDir} @ ${ref} (${sha})`);
|
|
12941
13240
|
console.log(" Installing workspace dependencies (bun install)\u2026");
|
|
12942
13241
|
await run("bun", ["install"], sourceDir);
|
|
12943
|
-
const distDir =
|
|
13242
|
+
const distDir = SOURCE_DIST_DIR;
|
|
12944
13243
|
console.log(" Building release dist (compiles the dashboard \u2014 needs RAM/CPU)\u2026");
|
|
12945
13244
|
await run(
|
|
12946
13245
|
"bun",
|
|
@@ -13071,20 +13370,33 @@ function dockerGap(state = dockerState()) {
|
|
|
13071
13370
|
function hasDockerCompose() {
|
|
13072
13371
|
return dockerGap() === null;
|
|
13073
13372
|
}
|
|
13074
|
-
function composeIsViableDefault() {
|
|
13075
|
-
return process.platform === "linux" && hasDockerCompose();
|
|
13076
|
-
}
|
|
13077
13373
|
function hasCmd(cmd) {
|
|
13078
13374
|
return spawnSync3(cmd, ["--version"], { stdio: "ignore" }).status === 0;
|
|
13079
13375
|
}
|
|
13080
13376
|
function shQuote(s2) {
|
|
13081
13377
|
return `'${s2.replace(/'/g, `'\\''`)}'`;
|
|
13082
13378
|
}
|
|
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 };
|
|
13383
|
+
const plan = systemCatalog.installs.docker({
|
|
13384
|
+
os: "linux",
|
|
13385
|
+
serviceManager: "systemd"
|
|
13386
|
+
});
|
|
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
|
+
}
|
|
13083
13396
|
async function ensureDocker(opts = {}) {
|
|
13084
13397
|
const notice = opts.onNotice ?? ((line) => process.stderr.write(` ${line}
|
|
13085
13398
|
`));
|
|
13086
|
-
const state =
|
|
13087
|
-
const gap = dockerGap(state);
|
|
13399
|
+
const { state, gap, wouldInstall, installCommand, startCommand } = dockerInstallPreview();
|
|
13088
13400
|
if (!gap) return true;
|
|
13089
13401
|
if (process.platform !== "linux") return false;
|
|
13090
13402
|
if (!gap.installable) {
|
|
@@ -13092,11 +13404,7 @@ async function ensureDocker(opts = {}) {
|
|
|
13092
13404
|
if (gap.hint) notice(gap.hint);
|
|
13093
13405
|
return false;
|
|
13094
13406
|
}
|
|
13095
|
-
|
|
13096
|
-
os: "linux",
|
|
13097
|
-
serviceManager: "systemd"
|
|
13098
|
-
});
|
|
13099
|
-
if (!plan.supported || !plan.installCommand) return false;
|
|
13407
|
+
if (!wouldInstall || !installCommand) return false;
|
|
13100
13408
|
const asRoot = typeof process.getuid === "function" && process.getuid() === 0;
|
|
13101
13409
|
const sudo = !asRoot && hasCmd("sudo") ? "sudo " : "";
|
|
13102
13410
|
notice("This can take 2-5 minutes on a small VPS (~150 MB of packages) and stays quiet while apt works.");
|
|
@@ -13119,8 +13427,8 @@ async function ensureDocker(opts = {}) {
|
|
|
13119
13427
|
child.on("error", () => done(1));
|
|
13120
13428
|
child.on("close", (code) => done(code ?? 1));
|
|
13121
13429
|
});
|
|
13122
|
-
if (await sh(
|
|
13123
|
-
if (
|
|
13430
|
+
if (await sh(installCommand) !== 0) return false;
|
|
13431
|
+
if (startCommand) await sh(startCommand);
|
|
13124
13432
|
const after = dockerGap();
|
|
13125
13433
|
if (!after) {
|
|
13126
13434
|
notice("Docker ready.");
|
|
@@ -13168,7 +13476,10 @@ services:
|
|
|
13168
13476
|
api:
|
|
13169
13477
|
image: \${OPENSHIP_IMAGE_REGISTRY:-ghcr.io/oblien}/openship-api:\${OPENSHIP_VERSION:-latest}
|
|
13170
13478
|
restart: unless-stopped
|
|
13171
|
-
|
|
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}"]
|
|
13172
13483
|
volumes:
|
|
13173
13484
|
- /var/run/docker.sock:/var/run/docker.sock
|
|
13174
13485
|
# Routing state shared with the edge, as HOST BIND MOUNTS (generated from
|
|
@@ -13203,7 +13514,8 @@ ${edgeVolumeYaml(" ")}
|
|
|
13203
13514
|
dashboard:
|
|
13204
13515
|
image: \${OPENSHIP_IMAGE_REGISTRY:-ghcr.io/oblien}/openship-dashboard:\${OPENSHIP_VERSION:-latest}
|
|
13205
13516
|
restart: unless-stopped
|
|
13206
|
-
|
|
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}"]
|
|
13207
13519
|
env_file: [.env]
|
|
13208
13520
|
environment:
|
|
13209
13521
|
NODE_ENV: production
|
|
@@ -13254,12 +13566,22 @@ function rewriteHostAuthorizedKeys(existing, pub) {
|
|
|
13254
13566
|
const kept = existing.split("\n").filter((line) => line.trim() && !line.includes(HOST_KEY_COMMENT));
|
|
13255
13567
|
return [...kept, hostKeyAuthLine(pub)].join("\n") + "\n";
|
|
13256
13568
|
}
|
|
13257
|
-
function
|
|
13258
|
-
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;
|
|
13259
13583
|
try {
|
|
13260
|
-
|
|
13261
|
-
mkdirSync3(sshDir, { recursive: true, mode: 448 });
|
|
13262
|
-
const keyPath = join7(sshDir, "id_ed25519");
|
|
13584
|
+
mkdirSync3(join7(COMPOSE_DIR, "host-ssh"), { recursive: true, mode: 448 });
|
|
13263
13585
|
if (!existsSync4(keyPath)) {
|
|
13264
13586
|
const g2 = spawnSync3(
|
|
13265
13587
|
"ssh-keygen",
|
|
@@ -13270,13 +13592,6 @@ function provisionHostSshChannel() {
|
|
|
13270
13592
|
}
|
|
13271
13593
|
const pub = readFileSync3(`${keyPath}.pub`, "utf8").trim();
|
|
13272
13594
|
if (!pub) return null;
|
|
13273
|
-
const user = (() => {
|
|
13274
|
-
try {
|
|
13275
|
-
return userInfo().username;
|
|
13276
|
-
} catch {
|
|
13277
|
-
return process.env.USER || process.env.LOGNAME || "root";
|
|
13278
|
-
}
|
|
13279
|
-
})();
|
|
13280
13595
|
const userSshDir = join7(homedir2(), ".ssh");
|
|
13281
13596
|
mkdirSync3(userSshDir, { recursive: true, mode: 448 });
|
|
13282
13597
|
const authKeys = join7(userSshDir, "authorized_keys");
|
|
@@ -13385,6 +13700,25 @@ function removeOrphanedStack(project) {
|
|
|
13385
13700
|
);
|
|
13386
13701
|
spawnSync3("docker", ["rm", "-f", ...orphans.map((o2) => o2.name)], { stdio: "ignore" });
|
|
13387
13702
|
}
|
|
13703
|
+
var EDGE_SWEEP_FORMAT = '{{.Names}} {{.Label "com.docker.compose.project"}} {{.Label "com.docker.compose.project.config_files"}}';
|
|
13704
|
+
function edgeNameNeedsReclaim(psStdout, project) {
|
|
13705
|
+
const row = psStdout.split("\n").map((l2) => l2.trim()).find((l2) => l2.split(" ")[0] === "openship-edge");
|
|
13706
|
+
if (!row) return false;
|
|
13707
|
+
const [, proj = "", configFiles = ""] = row.split(" ");
|
|
13708
|
+
const ours = proj === project && configFiles.split(",").some((f2) => f2.trim() === COMPOSE_FILE);
|
|
13709
|
+
return !ours;
|
|
13710
|
+
}
|
|
13711
|
+
function reconcileEdgeContainerName(project) {
|
|
13712
|
+
const r2 = spawnSync3("docker", ["ps", "-a", "--format", EDGE_SWEEP_FORMAT], {
|
|
13713
|
+
encoding: "utf8"
|
|
13714
|
+
});
|
|
13715
|
+
if (r2.status !== 0 || !r2.stdout) return;
|
|
13716
|
+
if (!edgeNameNeedsReclaim(r2.stdout, project)) return;
|
|
13717
|
+
console.log(
|
|
13718
|
+
" Replacing an unmanaged `openship-edge` container so compose can own it (config is on the host, so no routes are lost)."
|
|
13719
|
+
);
|
|
13720
|
+
spawnSync3("docker", ["rm", "-f", "openship-edge"], { stdio: "ignore" });
|
|
13721
|
+
}
|
|
13388
13722
|
function migrateLegacyEdgeVolumes(project) {
|
|
13389
13723
|
const pairs = [
|
|
13390
13724
|
{ volume: `${project}_openship_sites`, host: EDGE_SITES_HOST_DIR },
|
|
@@ -13447,9 +13781,26 @@ function keepConfig(prev, key, explicit) {
|
|
|
13447
13781
|
const carried = prev[key]?.trim();
|
|
13448
13782
|
return carried || void 0;
|
|
13449
13783
|
}
|
|
13784
|
+
var ACME_ENV_KEYS = [
|
|
13785
|
+
"OPENSHIP_ACME_EMAIL",
|
|
13786
|
+
"OPENSHIP_ACME_DIRECTORY_URL",
|
|
13787
|
+
"OPENSHIP_ACME_EAB_KID",
|
|
13788
|
+
"OPENSHIP_ACME_EAB_HMAC_KEY",
|
|
13789
|
+
"OPENSHIP_ACME_KEY_TYPE",
|
|
13790
|
+
"OPENSHIP_ACME_CA_BUNDLE",
|
|
13791
|
+
"OPENSHIP_ACME_TOS_AGREED"
|
|
13792
|
+
];
|
|
13793
|
+
function renderAcmeEnv(prev) {
|
|
13794
|
+
return ACME_ENV_KEYS.flatMap((key) => {
|
|
13795
|
+
const value = process.env[key]?.trim() || prev[key]?.trim();
|
|
13796
|
+
if (!value) return [];
|
|
13797
|
+
if (/[\r\n]/.test(value)) throw new Error(`${key} must be a single-line value`);
|
|
13798
|
+
return [`${key}=${value}`];
|
|
13799
|
+
});
|
|
13800
|
+
}
|
|
13450
13801
|
function resolveEnvConfig(prev, opts) {
|
|
13451
13802
|
const publicUrl = keepConfig(prev, "OPENSHIP_PUBLIC_URL", opts.publicUrl);
|
|
13452
|
-
const bindAddr = keepConfig(prev, "OPENSHIP_BIND_ADDR");
|
|
13803
|
+
const bindAddr = keepConfig(prev, "OPENSHIP_BIND_ADDR") ?? (publicUrl ? "0.0.0.0" : void 0);
|
|
13453
13804
|
return {
|
|
13454
13805
|
apiPort: keepConfig(prev, "API_PORT", opts.apiPort) ?? String(DEFAULT_API_PORT),
|
|
13455
13806
|
dashPort: keepConfig(prev, "DASHBOARD_PORT", opts.dashboardPort) ?? String(DEFAULT_DASHBOARD_PORT),
|
|
@@ -13480,7 +13831,7 @@ function composeEnvPorts() {
|
|
|
13480
13831
|
return { api: toPort(env.API_PORT), dashboard: toPort(env.DASHBOARD_PORT) };
|
|
13481
13832
|
}
|
|
13482
13833
|
function composeBindAddr() {
|
|
13483
|
-
return readEnvFile().OPENSHIP_BIND_ADDR?.trim() || "
|
|
13834
|
+
return readEnvFile().OPENSHIP_BIND_ADDR?.trim() || "127.0.0.1";
|
|
13484
13835
|
}
|
|
13485
13836
|
function composeTrustedOriginUrls() {
|
|
13486
13837
|
const env = readEnvFile();
|
|
@@ -13503,14 +13854,17 @@ function composeHeldPorts() {
|
|
|
13503
13854
|
}
|
|
13504
13855
|
return [...ports];
|
|
13505
13856
|
}
|
|
13506
|
-
async function resolveComposePorts(prefs) {
|
|
13507
|
-
return resolvePorts(
|
|
13508
|
-
|
|
13509
|
-
|
|
13510
|
-
|
|
13511
|
-
|
|
13512
|
-
|
|
13513
|
-
|
|
13857
|
+
async function resolveComposePorts(prefs, opts = {}) {
|
|
13858
|
+
return resolvePorts(
|
|
13859
|
+
{
|
|
13860
|
+
api: toPort(prefs.api),
|
|
13861
|
+
dashboard: toPort(prefs.dashboard),
|
|
13862
|
+
previous: composeEnvPorts(),
|
|
13863
|
+
bindAddr: composeBindAddr(),
|
|
13864
|
+
reclaimable: composeHeldPorts()
|
|
13865
|
+
},
|
|
13866
|
+
opts
|
|
13867
|
+
);
|
|
13514
13868
|
}
|
|
13515
13869
|
function renderEnv(opts, host, cfg) {
|
|
13516
13870
|
const prev = readEnvFile();
|
|
@@ -13525,7 +13879,7 @@ function renderEnv(opts, host, cfg) {
|
|
|
13525
13879
|
// (hides the local row). Written explicitly so the policy is visible in .env.
|
|
13526
13880
|
`OPENSHIP_HOST_CONTROL=${cfg.hostControl ? "true" : "false"}`,
|
|
13527
13881
|
`OPENSHIP_IMAGE_REGISTRY=${cfg.registry}`,
|
|
13528
|
-
`OPENSHIP_VERSION=${opts.version || (true ? "0.
|
|
13882
|
+
`OPENSHIP_VERSION=${opts.version || (true ? "0.6.1" : "latest")}`,
|
|
13529
13883
|
`POSTGRES_PASSWORD=${keepSecret(prev, "POSTGRES_PASSWORD")}`,
|
|
13530
13884
|
// Pinned once (see resolvePgData): fresh install → subdir, existing volume → root.
|
|
13531
13885
|
`OPENSHIP_PGDATA=${resolvePgData(prev)}`,
|
|
@@ -13537,7 +13891,10 @@ function renderEnv(opts, host, cfg) {
|
|
|
13537
13891
|
// the operator's domain at the dashboard through this (self-deploy.ts), and it
|
|
13538
13892
|
// silently defaults to 3001 when unset. Ports are dynamic now, so leaving it
|
|
13539
13893
|
// out publishes a domain routed to a port nothing is listening on.
|
|
13540
|
-
`OPENSHIP_DASHBOARD_PORT=${cfg.dashPort}
|
|
13894
|
+
`OPENSHIP_DASHBOARD_PORT=${cfg.dashPort}`,
|
|
13895
|
+
// Alternate CA/EAB values are operator configuration (including one secret),
|
|
13896
|
+
// so a routine `openship up`/upgrade must not silently discard them.
|
|
13897
|
+
...renderAcmeEnv(prev)
|
|
13541
13898
|
];
|
|
13542
13899
|
if (cfg.bindAddr) lines.push(`OPENSHIP_BIND_ADDR=${cfg.bindAddr}`);
|
|
13543
13900
|
if (cfg.publicUrl) lines.push(`OPENSHIP_PUBLIC_URL=${cfg.publicUrl}`);
|
|
@@ -13578,7 +13935,7 @@ function materialize(opts) {
|
|
|
13578
13935
|
mkdirSync3(COMPOSE_DIR, { recursive: true, mode: 448 });
|
|
13579
13936
|
const prev = readEnvFile();
|
|
13580
13937
|
const cfg = resolveEnvConfig(prev, opts);
|
|
13581
|
-
const host = cfg.hostControl
|
|
13938
|
+
const host = provisionHostSshChannel(cfg.hostControl);
|
|
13582
13939
|
let before = "";
|
|
13583
13940
|
try {
|
|
13584
13941
|
before = readFileSync3(ENV_FILE, "utf8");
|
|
@@ -13591,6 +13948,40 @@ function materialize(opts) {
|
|
|
13591
13948
|
if (buildDir) writeFileSync2(BUILD_FILE, renderBuildOverride(buildDir));
|
|
13592
13949
|
return { buildDir, cfg, envChanged: before !== "" && rendered !== before };
|
|
13593
13950
|
}
|
|
13951
|
+
var SECRET_ENV_KEY = /(PASSWORD|SECRET|TOKEN|HMAC_KEY)$/;
|
|
13952
|
+
function composePlan(opts) {
|
|
13953
|
+
const prev = readEnvFile();
|
|
13954
|
+
const cfg = resolveEnvConfig(prev, opts);
|
|
13955
|
+
const buildDir = opts.build === false ? null : sourceBuildDir();
|
|
13956
|
+
const hostChannel = plannedHostChannel(cfg.hostControl);
|
|
13957
|
+
const settings = [];
|
|
13958
|
+
const newSecrets = [];
|
|
13959
|
+
for (const line of renderEnv(opts, hostChannel, cfg).split("\n")) {
|
|
13960
|
+
const at2 = line.indexOf("=");
|
|
13961
|
+
if (at2 < 1 || line.startsWith("#")) continue;
|
|
13962
|
+
const key = line.slice(0, at2);
|
|
13963
|
+
if (!SECRET_ENV_KEY.test(key)) {
|
|
13964
|
+
settings.push({ key, value: line.slice(at2 + 1) });
|
|
13965
|
+
continue;
|
|
13966
|
+
}
|
|
13967
|
+
const minted = !prev[key];
|
|
13968
|
+
if (minted) newSecrets.push(key);
|
|
13969
|
+
settings.push({ key, value: minted ? "<generated on this run>" : "<preserved>" });
|
|
13970
|
+
}
|
|
13971
|
+
return {
|
|
13972
|
+
composeFile: COMPOSE_FILE,
|
|
13973
|
+
envFile: ENV_FILE,
|
|
13974
|
+
installMethodFile: INSTALL_METHOD_FILE,
|
|
13975
|
+
...buildDir ? { buildFile: BUILD_FILE } : {},
|
|
13976
|
+
yaml: COMPOSE_YAML,
|
|
13977
|
+
settings,
|
|
13978
|
+
newSecrets,
|
|
13979
|
+
existing: Object.keys(prev).length > 0,
|
|
13980
|
+
mountDirs: EDGE_CONTAINER_MOUNTS.map((m2) => m2.host),
|
|
13981
|
+
buildDir,
|
|
13982
|
+
hostChannel
|
|
13983
|
+
};
|
|
13984
|
+
}
|
|
13594
13985
|
function dbVolumeExists(project) {
|
|
13595
13986
|
const r2 = spawnSync3("docker", ["volume", "inspect", `${project}_postgres_data`], {
|
|
13596
13987
|
stdio: "ignore"
|
|
@@ -13613,6 +14004,23 @@ function composePrefetch(opts) {
|
|
|
13613
14004
|
}
|
|
13614
14005
|
return compose(["pull"]) === 0;
|
|
13615
14006
|
}
|
|
14007
|
+
function ensureEdgeMountDirs() {
|
|
14008
|
+
const failed = [];
|
|
14009
|
+
for (const { host } of EDGE_CONTAINER_MOUNTS) {
|
|
14010
|
+
try {
|
|
14011
|
+
mkdirSync3(host, { recursive: true });
|
|
14012
|
+
} catch (err) {
|
|
14013
|
+
if (err.code !== "EEXIST") failed.push(host);
|
|
14014
|
+
}
|
|
14015
|
+
}
|
|
14016
|
+
return failed;
|
|
14017
|
+
}
|
|
14018
|
+
function isRootlessDocker() {
|
|
14019
|
+
const r2 = spawnSync3("docker", ["info", "-f", "{{println .SecurityOptions}}"], {
|
|
14020
|
+
encoding: "utf8"
|
|
14021
|
+
});
|
|
14022
|
+
return r2.status === 0 && /rootless/i.test(r2.stdout ?? "");
|
|
14023
|
+
}
|
|
13616
14024
|
async function composeUp(opts) {
|
|
13617
14025
|
const { buildDir, cfg, envChanged } = materialize(opts);
|
|
13618
14026
|
const apiPort = cfg.apiPort;
|
|
@@ -13624,8 +14032,24 @@ async function composeUp(opts) {
|
|
|
13624
14032
|
const env = readEnvFile();
|
|
13625
14033
|
const project = env.COMPOSE_PROJECT_NAME || "openship";
|
|
13626
14034
|
removeOrphanedStack(project);
|
|
14035
|
+
reconcileEdgeContainerName(project);
|
|
13627
14036
|
migrateLegacyEdgeVolumes(project);
|
|
13628
14037
|
warnOrphanedVolumes(project);
|
|
14038
|
+
const unmakeableMounts = ensureEdgeMountDirs();
|
|
14039
|
+
if (unmakeableMounts.length && isRootlessDocker()) {
|
|
14040
|
+
const dirs = unmakeableMounts.join(" ");
|
|
14041
|
+
console.error(
|
|
14042
|
+
`
|
|
14043
|
+
Rootless Docker can't create the edge's host directories under root-owned paths:
|
|
14044
|
+
${unmakeableMounts.join("\n ")}
|
|
14045
|
+
|
|
14046
|
+
Create them once (owned by your user), then re-run \`openship up --compose\`:
|
|
14047
|
+
sudo mkdir -p ${dirs}
|
|
14048
|
+
sudo chown -R "$(id -un)" ${dirs}
|
|
14049
|
+
`
|
|
14050
|
+
);
|
|
14051
|
+
return { ok: false, apiPort, dashPort };
|
|
14052
|
+
}
|
|
13629
14053
|
if (dbVolumeExists(project)) {
|
|
13630
14054
|
reconcileDbPassword(env.POSTGRES_USER || "openship", env.POSTGRES_PASSWORD ?? "");
|
|
13631
14055
|
}
|
|
@@ -13698,6 +14122,7 @@ function composeRunning() {
|
|
|
13698
14122
|
return r2.status === 0 && r2.stdout.trim().length > 0;
|
|
13699
14123
|
}
|
|
13700
14124
|
function composeStart() {
|
|
14125
|
+
reconcileEdgeContainerName(readEnvFile().COMPOSE_PROJECT_NAME || "openship");
|
|
13701
14126
|
return compose(["up", "-d"], { quiet: true }) === 0;
|
|
13702
14127
|
}
|
|
13703
14128
|
function composeRestart() {
|
|
@@ -13728,7 +14153,7 @@ async function waitForEdgeRunning(tries) {
|
|
|
13728
14153
|
}
|
|
13729
14154
|
return false;
|
|
13730
14155
|
}
|
|
13731
|
-
async function importMigratedSites(apiPort, sites, certPems) {
|
|
14156
|
+
async function importMigratedSites(apiPort, sites, certPems, staticRootOverrides) {
|
|
13732
14157
|
const token = composeInternalToken();
|
|
13733
14158
|
if (!token) {
|
|
13734
14159
|
const error = "couldn't read the stack's internal token";
|
|
@@ -13753,7 +14178,7 @@ async function importMigratedSites(apiPort, sites, certPems) {
|
|
|
13753
14178
|
const r2 = await fetch(`http://127.0.0.1:${apiPort}/api/system/edge/import-sites`, {
|
|
13754
14179
|
method: "POST",
|
|
13755
14180
|
headers: { "Content-Type": "application/json", "X-Internal-Token": token },
|
|
13756
|
-
body: JSON.stringify({ sites, certPems }),
|
|
14181
|
+
body: JSON.stringify({ sites, certPems, staticRootOverrides }),
|
|
13757
14182
|
signal: AbortSignal.timeout(12e4)
|
|
13758
14183
|
});
|
|
13759
14184
|
const data = await r2.json().catch(() => ({}));
|
|
@@ -13808,8 +14233,11 @@ async function importMigratedSites(apiPort, sites, certPems) {
|
|
|
13808
14233
|
export {
|
|
13809
14234
|
OS_DIR,
|
|
13810
14235
|
IS_ALT_HOME,
|
|
13811
|
-
|
|
13812
|
-
|
|
14236
|
+
DATA_DIR,
|
|
14237
|
+
LOG_DIR,
|
|
14238
|
+
AUTH_SECRET_FILE,
|
|
14239
|
+
INTERNAL_TOKEN_FILE,
|
|
14240
|
+
PORTS_FILE,
|
|
13813
14241
|
saveInstanceUrl,
|
|
13814
14242
|
readInstanceUrl,
|
|
13815
14243
|
readStoredPorts,
|
|
@@ -13817,6 +14245,10 @@ export {
|
|
|
13817
14245
|
storedDashboardPort,
|
|
13818
14246
|
portMoveNotice,
|
|
13819
14247
|
resolvePorts,
|
|
14248
|
+
DEFAULT_REPO,
|
|
14249
|
+
SOURCE_CHECKOUT_DIR,
|
|
14250
|
+
SOURCE_DIST_DIR,
|
|
14251
|
+
has,
|
|
13820
14252
|
shortSha,
|
|
13821
14253
|
prepareFromSource,
|
|
13822
14254
|
readSourceInstall,
|
|
@@ -13825,11 +14257,12 @@ export {
|
|
|
13825
14257
|
readInstallMethod,
|
|
13826
14258
|
dockerGap,
|
|
13827
14259
|
hasDockerCompose,
|
|
13828
|
-
|
|
14260
|
+
dockerInstallPreview,
|
|
13829
14261
|
ensureDocker,
|
|
13830
14262
|
composeTrustedOriginUrls,
|
|
13831
14263
|
resolveComposePorts,
|
|
13832
14264
|
sourceBuildDir,
|
|
14265
|
+
composePlan,
|
|
13833
14266
|
composePrefetch,
|
|
13834
14267
|
composeUp,
|
|
13835
14268
|
composeDown,
|