openship 0.6.1 → 0.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -2
- package/dist/{bare-IWS2QHJ3.js → bare-OXJVW3AK.js} +11 -9
- package/dist/{chunk-VZ2ZHPS2.js → chunk-2YM3NI2E.js} +3 -9
- package/dist/chunk-4I2BM2DT.js +68 -0
- package/dist/{chunk-2NZSUXOB.js → chunk-5DMWSTOM.js} +310 -1459
- package/dist/{chunk-3FYIXVX5.js → chunk-77QDF6UD.js} +581 -122
- package/dist/{chunk-IOT5ZX23.js → chunk-A4GXYE4L.js} +449 -383
- package/dist/chunk-CBLD2HS5.js +428 -0
- package/dist/chunk-ELGRMZOW.js +25 -0
- package/dist/{chunk-TWJCLD4W.js → chunk-FE7ZZQSE.js} +5 -5
- package/dist/{chunk-3NZICQHF.js → chunk-HKRQXPTI.js} +31 -8
- package/dist/{chunk-IXM5245F.js → chunk-LFE5KQ7Y.js} +553 -538
- package/dist/{chunk-VDDMADOF.js → chunk-LH2PLRQO.js} +16 -2
- package/dist/{chunk-EMXI4UCU.js → chunk-LUKYPVJ4.js} +29 -10
- package/dist/{chunk-UC7YIEHT.js → chunk-MLYSEZQB.js} +352 -47
- package/dist/chunk-NK5N5LXN.js +1681 -0
- package/dist/{chunk-RRJJGVKE.js → chunk-OBPKYXQL.js} +75 -17
- package/dist/{chunk-6ODUBKUB.js → chunk-PCPVTH6M.js} +78 -88
- package/dist/chunk-Q7WGTQQ5.js +88 -0
- package/dist/{chunk-EW6KB2J7.js → chunk-QVW76WDL.js} +440 -74
- package/dist/{chunk-YIPMDGG6.js → chunk-RSR7PFIQ.js} +79 -23
- package/dist/{chunk-3LDWWY7U.js → chunk-SM26HDS2.js} +41 -7
- package/dist/{chunk-OTLSJYAC.js → chunk-UJ6JZM65.js} +5185 -341
- package/dist/chunk-UVNMQM7W.js +444 -0
- package/dist/chunk-VTGPEC6B.js +308 -0
- package/dist/chunk-WVAML4H6.js +2145 -0
- package/dist/{chunk-JIMUYEKN.js → chunk-YIMOCZBZ.js} +1 -1
- package/dist/chunk-YMMQRE5J.js +292 -0
- package/dist/cloud-75VIT4KV.js +37 -0
- package/dist/{detect-ALF4GXRX.js → detect-OIE5V3VZ.js} +3 -3
- package/dist/{docker-QECQJ5P4.js → docker-VTR2GK5Y.js} +16 -8
- package/dist/{docker-edge-executor-62N64MGS.js → docker-edge-executor-22IBXNYJ.js} +5 -5
- package/dist/{edge-import-SAYTRJLO.js → edge-import-WJUB4M7F.js} +27 -23
- package/dist/edge-preflight-GBNCPNVU.js +42 -0
- package/dist/{ensure-container-edge-CFSCPGZK.js → ensure-container-edge-KPI2CVXJ.js} +9 -8
- package/dist/{executor-PT6UEJ5M.js → executor-7LXXZPBR.js} +15 -5
- package/dist/host-channel-preflight-KE6OONYK.js +50 -0
- package/dist/index.js +3783 -4221
- package/dist/{nginx-YOEEFBTE.js → nginx-KUFMLMAC.js} +13 -5
- package/dist/node-bootstrap.js +241 -0
- package/dist/node-entry.js +52 -1
- package/dist/{openresty-lua-WGBO2M3Z.js → openresty-lua-ATTH35BE.js} +8 -2
- package/dist/privilege-I4W3ZNZK.js +19 -0
- package/dist/server/engine/samples/amavisd/amavisd.conf +25 -0
- package/dist/server/engine/samples/fail2ban/jail.d/sshd.local +4 -1
- package/dist/server/engine/samples/postfix/master.cf +14 -0
- package/dist/server/index.js +77659 -61909
- package/dist/server/migrations/0098_instance_product_mode.sql +12 -0
- package/dist/server/migrations/0099_mail_webmail_project.sql +24 -0
- package/dist/server/migrations/0100_server_ssh_private_key.sql +11 -0
- package/dist/server/migrations/0101_mail_resume_step.sql +12 -0
- package/dist/server/migrations/0102_project_workload_axes.sql +19 -0
- package/dist/server/migrations/0103_dns_credential.sql +41 -0
- package/dist/server/migrations/0104_instance_host_control.sql +15 -0
- package/dist/server/migrations/0105_mail_inbound_rules.sql +80 -0
- package/dist/server/migrations/0106_mcp_call_tracking.sql +23 -0
- package/dist/server/migrations/0107_build_minute_metering_indexes.sql +20 -0
- package/dist/server/migrations/0108_credential.sql +68 -0
- package/dist/server/migrations/meta/_journal.json +77 -0
- package/dist/{setup-M2ZDGH3O.js → setup-XGLGUUFW.js} +10 -9
- package/dist/ssh-client-JBMA7SN4.js +28 -0
- package/dist/up-K4BV7466.js +57 -0
- package/package.json +4 -2
- package/dist/chunk-IW2UKBVF.js +0 -144
- package/dist/chunk-ZCFVHA25.js +0 -89
- package/dist/cloud-M2YLWEXO.js +0 -31
- /package/dist/{dist-es-JGBPABJI.js → dist-es-AXFRS7PS.js} +0 -0
|
@@ -2,17 +2,32 @@
|
|
|
2
2
|
":" //# ; exec "$(command -v node || command -v bun)" "$0" "$@"
|
|
3
3
|
import { createRequire as __ospCreateRequire } from "node:module";
|
|
4
4
|
const require = __ospCreateRequire(import.meta.url);
|
|
5
|
-
import {
|
|
6
|
-
ENV_KEY,
|
|
7
|
-
ENV_SECRET,
|
|
8
|
-
fromEnv
|
|
9
|
-
} from "./chunk-XZYFZ6A5.js";
|
|
10
5
|
import {
|
|
11
6
|
SignatureV4MultiRegion,
|
|
12
7
|
XmlNode,
|
|
13
8
|
XmlShapeDeserializer,
|
|
14
9
|
XmlText
|
|
15
10
|
} from "./chunk-QWHBRY2K.js";
|
|
11
|
+
import {
|
|
12
|
+
OS_DIR
|
|
13
|
+
} from "./chunk-ELGRMZOW.js";
|
|
14
|
+
import {
|
|
15
|
+
AwsCrc32,
|
|
16
|
+
__awaiter,
|
|
17
|
+
__generator,
|
|
18
|
+
__values,
|
|
19
|
+
convertToBuffer,
|
|
20
|
+
eventStreamSerdeProvider2 as eventStreamSerdeProvider,
|
|
21
|
+
isEmptyData,
|
|
22
|
+
numToUint8,
|
|
23
|
+
resolveEventStreamSerdeConfig,
|
|
24
|
+
uint32ArrayFrom
|
|
25
|
+
} from "./chunk-ONKEYSMY.js";
|
|
26
|
+
import {
|
|
27
|
+
ENV_KEY,
|
|
28
|
+
ENV_SECRET,
|
|
29
|
+
fromEnv
|
|
30
|
+
} from "./chunk-XZYFZ6A5.js";
|
|
16
31
|
import {
|
|
17
32
|
AwsSdkSigV4ASigner,
|
|
18
33
|
AwsSdkSigV4Signer,
|
|
@@ -99,24 +114,6 @@ import {
|
|
|
99
114
|
resolveRegionConfig,
|
|
100
115
|
toEndpointV1
|
|
101
116
|
} from "./chunk-3NQQXMDR.js";
|
|
102
|
-
import {
|
|
103
|
-
CloudRuntime
|
|
104
|
-
} from "./chunk-RRJJGVKE.js";
|
|
105
|
-
import {
|
|
106
|
-
systemCatalog
|
|
107
|
-
} from "./chunk-IOT5ZX23.js";
|
|
108
|
-
import {
|
|
109
|
-
AwsCrc32,
|
|
110
|
-
__awaiter,
|
|
111
|
-
__generator,
|
|
112
|
-
__values,
|
|
113
|
-
convertToBuffer,
|
|
114
|
-
eventStreamSerdeProvider2 as eventStreamSerdeProvider,
|
|
115
|
-
isEmptyData,
|
|
116
|
-
numToUint8,
|
|
117
|
-
resolveEventStreamSerdeConfig,
|
|
118
|
-
uint32ArrayFrom
|
|
119
|
-
} from "./chunk-ONKEYSMY.js";
|
|
120
117
|
import {
|
|
121
118
|
Hash,
|
|
122
119
|
NumericValue,
|
|
@@ -135,6 +132,12 @@ import {
|
|
|
135
132
|
toUint8Array,
|
|
136
133
|
toUtf8
|
|
137
134
|
} from "./chunk-ZWLDLTJP.js";
|
|
135
|
+
import {
|
|
136
|
+
CloudRuntime
|
|
137
|
+
} from "./chunk-OBPKYXQL.js";
|
|
138
|
+
import {
|
|
139
|
+
OPENRESTY_DEFAULT_PATHS
|
|
140
|
+
} from "./chunk-RSR7PFIQ.js";
|
|
138
141
|
import {
|
|
139
142
|
DockerRuntime,
|
|
140
143
|
daemonConnectionFrom,
|
|
@@ -143,34 +146,29 @@ import {
|
|
|
143
146
|
scopedVolumeName,
|
|
144
147
|
startAttachStream,
|
|
145
148
|
startExecStream
|
|
146
|
-
} from "./chunk-
|
|
149
|
+
} from "./chunk-77QDF6UD.js";
|
|
147
150
|
import {
|
|
148
151
|
BareRuntime
|
|
149
|
-
} from "./chunk-
|
|
152
|
+
} from "./chunk-LFE5KQ7Y.js";
|
|
150
153
|
import {
|
|
151
|
-
|
|
152
|
-
} from "./chunk-
|
|
154
|
+
createExecutor
|
|
155
|
+
} from "./chunk-MLYSEZQB.js";
|
|
153
156
|
import {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
invalidateEdgeContainer,
|
|
157
|
-
sanitizeEdgeVhosts
|
|
158
|
-
} from "./chunk-VZ2ZHPS2.js";
|
|
157
|
+
envOps
|
|
158
|
+
} from "./chunk-YMMQRE5J.js";
|
|
159
159
|
import {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
160
|
+
ENVIRONMENT_PROBE_SCRIPT,
|
|
161
|
+
ENVIRONMENT_PROBE_TIMEOUT_MS,
|
|
162
|
+
cacheEnvironment,
|
|
163
|
+
parseEnvironmentProbe,
|
|
164
|
+
profileIsFresh
|
|
165
|
+
} from "./chunk-UVNMQM7W.js";
|
|
164
166
|
import {
|
|
165
|
-
|
|
167
|
+
FAMILY_BY_DISTRO,
|
|
166
168
|
isAwsS3Endpoint,
|
|
167
169
|
safeErrorMessage,
|
|
168
170
|
withTimeout
|
|
169
|
-
} from "./chunk-
|
|
170
|
-
|
|
171
|
-
// src/lib/edge-import.ts
|
|
172
|
-
import chalk from "chalk";
|
|
173
|
-
import ora from "ora";
|
|
171
|
+
} from "./chunk-UJ6JZM65.js";
|
|
174
172
|
|
|
175
173
|
// ../../packages/adapters/src/runtime/image-transfer.ts
|
|
176
174
|
import { Transform } from "stream";
|
|
@@ -194,8 +192,10 @@ var MAIL_CONTAINER_MOUNTS = [
|
|
|
194
192
|
{ host: `${MAIL_HOST_STATE_DIR}/config/dovecot`, container: "/etc/dovecot", seed: true },
|
|
195
193
|
{ host: `${MAIL_HOST_STATE_DIR}/config/amavis`, container: "/etc/amavis/conf.d", seed: true },
|
|
196
194
|
// ClamAV signatures — bind-mounted so a pull doesn't force a multi-hundred-MB
|
|
197
|
-
// freshclam re-download and delay readiness.
|
|
198
|
-
|
|
195
|
+
// freshclam re-download and delay readiness. `seed: true` because that same mount
|
|
196
|
+
// HIDES the database baked into the image: with nothing copying it across, clamd
|
|
197
|
+
// finds an empty database directory and exits (issue #565).
|
|
198
|
+
{ host: `${MAIL_HOST_STATE_DIR}/clamav`, container: "/var/lib/clamav", seed: true },
|
|
199
199
|
{ host: "/etc/letsencrypt", container: "/etc/letsencrypt", readonly: true }
|
|
200
200
|
];
|
|
201
201
|
var MAIL_DB_HOST_DATA_DIR = `${MAIL_HOST_STATE_DIR}/pgdata`;
|
|
@@ -204,6 +204,11 @@ var MAIL_DB_PGDATA = `${MAIL_DB_CONTAINER_DATA_DIR}/pgdata`;
|
|
|
204
204
|
var MAIL_HOST_PATHS = {
|
|
205
205
|
saslPasswd: `${MAIL_HOST_STATE_DIR}/config/postfix/sasl_passwd`,
|
|
206
206
|
senderRelayhost: `${MAIL_HOST_STATE_DIR}/config/postfix/sender_relayhost`,
|
|
207
|
+
// Per-nexthop TLS policy. Only split ("selected"-scope) relaying writes it: the
|
|
208
|
+
// relay hop must be `encrypt` so SASL credentials never cross a plaintext
|
|
209
|
+
// connection, while direct-to-MX delivery for the un-relayed domains stays
|
|
210
|
+
// opportunistic. A GLOBAL smtp_tls_security_level can't express both.
|
|
211
|
+
relayTlsPolicy: `${MAIL_HOST_STATE_DIR}/config/postfix/openship_tls_policy`,
|
|
207
212
|
amavisUserConf: `${MAIL_HOST_STATE_DIR}/config/amavis/50-user`
|
|
208
213
|
};
|
|
209
214
|
|
|
@@ -212,6 +217,63 @@ var MAIL_DB_IMAGE = process.env.OPENSHIP_MAIL_DB_IMAGE?.trim() || "postgres:16-a
|
|
|
212
217
|
var ENGINE_ENV_FILE = `${MAIL_HOST_STATE_DIR}/engine.env`;
|
|
213
218
|
var DB_ENV_FILE = `${MAIL_HOST_STATE_DIR}/db.env`;
|
|
214
219
|
|
|
220
|
+
// ../../packages/adapters/src/system/environment-local.ts
|
|
221
|
+
import { spawnSync } from "child_process";
|
|
222
|
+
var cached = null;
|
|
223
|
+
function unmeasurable(reason) {
|
|
224
|
+
return {
|
|
225
|
+
...parseEnvironmentProbe({ text: "", failed: true, unanswered: true }),
|
|
226
|
+
unsupportedReason: reason
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function probeLocalHost() {
|
|
230
|
+
if (process.platform === "win32") {
|
|
231
|
+
return unmeasurable(
|
|
232
|
+
"Openship cannot provision this machine: it runs Windows, and Openship's host provisioning is POSIX-only. Deploying to a remote Linux server from here works."
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
const result = spawnSync("sh", ["-c", ENVIRONMENT_PROBE_SCRIPT], {
|
|
236
|
+
encoding: "utf8",
|
|
237
|
+
timeout: ENVIRONMENT_PROBE_TIMEOUT_MS,
|
|
238
|
+
// `sh -c` and not the login shell: `wrapLocalBuildCommand`'s `-lc` exists so user
|
|
239
|
+
// builds see a normal profile, but a host probe wants the plain POSIX behaviour the
|
|
240
|
+
// remote path gets, without a dotfile's `echo` in the middle of the output.
|
|
241
|
+
windowsHide: true
|
|
242
|
+
});
|
|
243
|
+
if (result.error) {
|
|
244
|
+
return unmeasurable(
|
|
245
|
+
`Openship could not run a shell on this machine to detect it: ${safeErrorMessage(result.error)}`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
if (result.signal || typeof result.status !== "number") {
|
|
249
|
+
return unmeasurable(
|
|
250
|
+
`Openship could not measure this machine: the detection probe was killed${result.signal ? ` by ${result.signal}` : ""} before it finished.`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
const stdout = result.stdout ?? "";
|
|
254
|
+
return parseEnvironmentProbe({
|
|
255
|
+
// The script always exits 0, so a non-zero status means something ran instead of it
|
|
256
|
+
// — the same signal the SSH path uses, and stderr is what said so.
|
|
257
|
+
text: stdout.trim() ? stdout : result.stderr ?? "",
|
|
258
|
+
failed: result.status !== 0,
|
|
259
|
+
// Every way of not answering is refused above, so the shell exited on its own and
|
|
260
|
+
// whatever it said is this machine's.
|
|
261
|
+
unanswered: false
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
function resolveLocalEnvironmentSync() {
|
|
265
|
+
if (cached && profileIsFresh(cached.measuredAt)) return cached.profile;
|
|
266
|
+
const profile = probeLocalHost();
|
|
267
|
+
cached = { measuredAt: Date.now(), profile };
|
|
268
|
+
cacheEnvironment(createExecutor(), profile);
|
|
269
|
+
return profile;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ../../packages/adapters/src/system/modules/types.ts
|
|
273
|
+
var TARGETABLE_FAMILIES = new Set(
|
|
274
|
+
Object.values(FAMILY_BY_DISTRO)
|
|
275
|
+
);
|
|
276
|
+
|
|
215
277
|
// ../../packages/adapters/src/system/modules/verify.ts
|
|
216
278
|
import { createHash, createPublicKey, verify as cryptoVerify } from "crypto";
|
|
217
279
|
|
|
@@ -219,10 +281,6 @@ import { createHash, createPublicKey, verify as cryptoVerify } from "crypto";
|
|
|
219
281
|
var CATALOG_BASE_URL = process.env.OPENSHIP_MODULE_CATALOG_BASE_URL?.trim() || "https://raw.githubusercontent.com/openship/native-modules";
|
|
220
282
|
var CATALOG_REF = process.env.OPENSHIP_MODULE_CATALOG_REF?.trim() || "main";
|
|
221
283
|
|
|
222
|
-
// ../../packages/adapters/src/system/reachability.ts
|
|
223
|
-
import { connect } from "net";
|
|
224
|
-
import { request } from "http";
|
|
225
|
-
|
|
226
284
|
// ../../packages/adapters/src/backup/registry.ts
|
|
227
285
|
var executorFactories = /* @__PURE__ */ new Map();
|
|
228
286
|
function registerExecutor(runtimeName, factory) {
|
|
@@ -747,12 +805,14 @@ var DockerBackupExecutor = class {
|
|
|
747
805
|
try {
|
|
748
806
|
const data = await this.dockerode.getContainer(service.containerId).inspect();
|
|
749
807
|
const mounts = data.Mounts ?? [];
|
|
750
|
-
return mounts.filter((m2) => m2.Type === "volume" || m2.Type === "bind").map(
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
808
|
+
return mounts.filter((m2) => m2.Type === "volume" || m2.Type === "bind").map(
|
|
809
|
+
(m2, i2) => ({
|
|
810
|
+
id: m2.Name ?? m2.Source ?? `mount-${i2}`,
|
|
811
|
+
target: m2.Destination ?? "",
|
|
812
|
+
source: m2.Name ?? m2.Source ?? "",
|
|
813
|
+
type: m2.Type ?? "volume"
|
|
814
|
+
})
|
|
815
|
+
);
|
|
756
816
|
} catch {
|
|
757
817
|
}
|
|
758
818
|
}
|
|
@@ -784,7 +844,9 @@ var DockerBackupExecutor = class {
|
|
|
784
844
|
Env: opts?.env ? Object.entries(opts.env).map(([k2, v2]) => `${k2}=${v2}`) : void 0
|
|
785
845
|
});
|
|
786
846
|
const stream = await exec.start({ stdin: false });
|
|
787
|
-
return this.attachDemuxed(this.dockerode, exec.id, stream, opts
|
|
847
|
+
return this.attachDemuxed(this.dockerode, exec.id, stream, opts, {
|
|
848
|
+
label: `exec in service ${service.name}`
|
|
849
|
+
});
|
|
788
850
|
}
|
|
789
851
|
async streamPath(service, sourceId, opts) {
|
|
790
852
|
const sources = await this.listSources(service);
|
|
@@ -796,10 +858,7 @@ var DockerBackupExecutor = class {
|
|
|
796
858
|
throw new Error(`Backup source "${sourceId}" is tmpfs \u2014 not backupable`);
|
|
797
859
|
}
|
|
798
860
|
const compression = opts?.compression ?? "zstd";
|
|
799
|
-
const excludeArgs = (opts?.exclude ?? []).flatMap((p2) => [
|
|
800
|
-
"--exclude",
|
|
801
|
-
shellEscape4(p2)
|
|
802
|
-
]);
|
|
861
|
+
const excludeArgs = (opts?.exclude ?? []).flatMap((p2) => ["--exclude", shellEscape4(p2)]);
|
|
803
862
|
const tarFlags = compressionFlag(compression);
|
|
804
863
|
const tarCmd = compression === "zstd" ? `tar -c${tarFlags} -C /mnt ${excludeArgs.join(" ")} . | zstd -c -3` : `tar -c${tarFlags} -C /mnt ${excludeArgs.join(" ")} .`;
|
|
805
864
|
const helperImage = compression === "zstd" ? "alpine:3" : HELPER_IMAGE;
|
|
@@ -824,7 +883,11 @@ var DockerBackupExecutor = class {
|
|
|
824
883
|
return this.handOffHelper(
|
|
825
884
|
{
|
|
826
885
|
Image: helperImage,
|
|
827
|
-
Cmd: [
|
|
886
|
+
Cmd: [
|
|
887
|
+
"sh",
|
|
888
|
+
"-c",
|
|
889
|
+
compression === "zstd" ? `apk add --no-cache zstd >/dev/null 2>&1; ${tarCmd}` : tarCmd
|
|
890
|
+
],
|
|
828
891
|
HostConfig: hostConfig,
|
|
829
892
|
AttachStdout: true,
|
|
830
893
|
AttachStderr: true,
|
|
@@ -1170,7 +1233,7 @@ var DockerBackupExecutor = class {
|
|
|
1170
1233
|
} catch {
|
|
1171
1234
|
}
|
|
1172
1235
|
}, opts.timeoutMs) : null;
|
|
1173
|
-
return new Promise((
|
|
1236
|
+
return new Promise((resolve2, reject) => {
|
|
1174
1237
|
body.on("error", (err) => {
|
|
1175
1238
|
if (timer) clearTimeout(timer);
|
|
1176
1239
|
try {
|
|
@@ -1182,7 +1245,7 @@ var DockerBackupExecutor = class {
|
|
|
1182
1245
|
stream.on("end", async () => {
|
|
1183
1246
|
if (timer) clearTimeout(timer);
|
|
1184
1247
|
try {
|
|
1185
|
-
|
|
1248
|
+
resolve2({
|
|
1186
1249
|
code: await resolveExecExitCode(exec, exec.id),
|
|
1187
1250
|
stderr: Buffer.concat(stderrChunks).toString("utf8").slice(0, 16 * 1024)
|
|
1188
1251
|
});
|
|
@@ -1230,7 +1293,10 @@ var DockerBackupExecutor = class {
|
|
|
1230
1293
|
}
|
|
1231
1294
|
/** dockerode `exec.start` returns a multiplexed stream — stdout +
|
|
1232
1295
|
* stderr interleaved with frame headers. demux into clean streams. */
|
|
1233
|
-
attachDemuxed(docker, execId, stream,
|
|
1296
|
+
attachDemuxed(docker, execId, stream, opts, meta) {
|
|
1297
|
+
const label = meta?.label ?? "exec";
|
|
1298
|
+
const idleMs = opts?.idleTimeoutMs ?? DEFAULT_HELPER_IDLE_MS;
|
|
1299
|
+
const timeoutMs = opts?.timeoutMs ?? DEFAULT_HELPER_TIMEOUT_MS;
|
|
1234
1300
|
const stdout = new PassThrough();
|
|
1235
1301
|
const stderrChunks = [];
|
|
1236
1302
|
const stderrSink = new PassThrough();
|
|
@@ -1238,27 +1304,47 @@ var DockerBackupExecutor = class {
|
|
|
1238
1304
|
if (stderrChunks.length < 16) stderrChunks.push(chunk);
|
|
1239
1305
|
});
|
|
1240
1306
|
docker.modem.demuxStream(stream, stdout, stderrSink);
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1307
|
+
let sinksEnded = false;
|
|
1308
|
+
const endSinks = () => {
|
|
1309
|
+
if (sinksEnded) return;
|
|
1310
|
+
sinksEnded = true;
|
|
1311
|
+
stdout.end();
|
|
1312
|
+
stderrSink.end();
|
|
1313
|
+
};
|
|
1314
|
+
stream.on("end", endSinks);
|
|
1315
|
+
stream.on("close", endSinks);
|
|
1316
|
+
const watchdog = createIdleWatchdog(
|
|
1317
|
+
idleMs,
|
|
1318
|
+
`${label} produced no data for ${Math.round(idleMs / 1e3)}s and was abandoned.`
|
|
1319
|
+
);
|
|
1320
|
+
stream.on("data", () => watchdog.touch());
|
|
1321
|
+
const awaitExit = (async () => {
|
|
1322
|
+
const onEnd = new Promise((resolve2, reject) => {
|
|
1323
|
+
stream.on("end", async () => {
|
|
1324
|
+
try {
|
|
1325
|
+
resolve2({
|
|
1326
|
+
code: await resolveExecExitCode(docker.getExec(execId), execId),
|
|
1327
|
+
stderr: Buffer.concat(stderrChunks).toString("utf8").slice(0, 16 * 1024)
|
|
1328
|
+
});
|
|
1329
|
+
} catch (err) {
|
|
1330
|
+
reject(err);
|
|
1331
|
+
}
|
|
1332
|
+
});
|
|
1333
|
+
stream.on("error", (err) => reject(err));
|
|
1260
1334
|
});
|
|
1261
|
-
|
|
1335
|
+
try {
|
|
1336
|
+
return await withTimeout(
|
|
1337
|
+
Promise.race([onEnd, watchdog.promise]),
|
|
1338
|
+
timeoutMs,
|
|
1339
|
+
`${label} exceeded its ${Math.round(timeoutMs / 1e3)}s ceiling and was abandoned.`
|
|
1340
|
+
);
|
|
1341
|
+
} catch (err) {
|
|
1342
|
+
stdout.destroy(err);
|
|
1343
|
+
throw err;
|
|
1344
|
+
} finally {
|
|
1345
|
+
watchdog.dispose();
|
|
1346
|
+
}
|
|
1347
|
+
})();
|
|
1262
1348
|
return { stdout, awaitExit };
|
|
1263
1349
|
}
|
|
1264
1350
|
demuxContainerStream(container, stream, opts) {
|
|
@@ -1359,7 +1445,7 @@ var CloudBackupExecutor = class {
|
|
|
1359
1445
|
const stdout = new Readable2({ read() {
|
|
1360
1446
|
} });
|
|
1361
1447
|
let stderrBuf = "";
|
|
1362
|
-
const awaitExit = new Promise((
|
|
1448
|
+
const awaitExit = new Promise((resolve2, reject) => {
|
|
1363
1449
|
const timer = opts?.timeoutMs ? setTimeout(() => {
|
|
1364
1450
|
stdout.destroy(new Error(`exec timed out after ${opts.timeoutMs}ms`));
|
|
1365
1451
|
reject(new Error(`exec timed out after ${opts.timeoutMs}ms`));
|
|
@@ -1392,7 +1478,7 @@ var CloudBackupExecutor = class {
|
|
|
1392
1478
|
}
|
|
1393
1479
|
stdout.push(null);
|
|
1394
1480
|
if (timer) clearTimeout(timer);
|
|
1395
|
-
|
|
1481
|
+
resolve2({ code: exitCode, stderr: stderrBuf });
|
|
1396
1482
|
} catch (err) {
|
|
1397
1483
|
if (timer) clearTimeout(timer);
|
|
1398
1484
|
stdout.destroy(err);
|
|
@@ -2370,8 +2456,8 @@ var flexibleChecksumsMiddleware = (config, middlewareConfig) => (next, context)
|
|
|
2370
2456
|
if (hasHeaderWithPrefix("x-amz-checksum-", args.request.headers)) {
|
|
2371
2457
|
return next(args);
|
|
2372
2458
|
}
|
|
2373
|
-
const { request
|
|
2374
|
-
const { body: requestBody, headers } =
|
|
2459
|
+
const { request, input } = args;
|
|
2460
|
+
const { body: requestBody, headers } = request;
|
|
2375
2461
|
const { base64Encoder, streamHasher } = config;
|
|
2376
2462
|
const { requestChecksumRequired, requestAlgorithmMember } = middlewareConfig;
|
|
2377
2463
|
const requestChecksumCalculation = await config.requestChecksumCalculation();
|
|
@@ -2442,7 +2528,7 @@ var flexibleChecksumsMiddleware = (config, middlewareConfig) => (next, context)
|
|
|
2442
2528
|
const result = await next({
|
|
2443
2529
|
...args,
|
|
2444
2530
|
request: {
|
|
2445
|
-
...
|
|
2531
|
+
...request,
|
|
2446
2532
|
headers: updatedHeaders,
|
|
2447
2533
|
body: updatedBody
|
|
2448
2534
|
}
|
|
@@ -2638,9 +2724,9 @@ var CONTENT_LENGTH_HEADER = "content-length";
|
|
|
2638
2724
|
var DECODED_CONTENT_LENGTH_HEADER = "x-amz-decoded-content-length";
|
|
2639
2725
|
function checkContentLengthHeader() {
|
|
2640
2726
|
return (next, context) => async (args) => {
|
|
2641
|
-
const { request
|
|
2642
|
-
if (HttpRequest.isInstance(
|
|
2643
|
-
if (!(CONTENT_LENGTH_HEADER in
|
|
2727
|
+
const { request } = args;
|
|
2728
|
+
if (HttpRequest.isInstance(request)) {
|
|
2729
|
+
if (!(CONTENT_LENGTH_HEADER in request.headers) && !(DECODED_CONTENT_LENGTH_HEADER in request.headers)) {
|
|
2644
2730
|
const message = `Are you using a Stream of unknown length as the Body of a PutObject request? Consider using Upload instead from @aws-sdk/lib-storage.`;
|
|
2645
2731
|
if (typeof context?.logger?.warn === "function" && !(context.logger instanceof NoOpLogger)) {
|
|
2646
2732
|
context.logger.warn(message);
|
|
@@ -2956,8 +3042,8 @@ var getS3ExpressPlugin = (options) => ({
|
|
|
2956
3042
|
});
|
|
2957
3043
|
|
|
2958
3044
|
// ../../node_modules/@aws-sdk/middleware-sdk-s3/dist-es/submodules/s3/middleware-s3-express/functions/signS3Express.js
|
|
2959
|
-
var signS3Express = async (s3ExpressIdentity, signingOptions,
|
|
2960
|
-
const signedRequest = await sigV4MultiRegionSigner.signWithCredentials(
|
|
3045
|
+
var signS3Express = async (s3ExpressIdentity, signingOptions, request, sigV4MultiRegionSigner) => {
|
|
3046
|
+
const signedRequest = await sigV4MultiRegionSigner.signWithCredentials(request, s3ExpressIdentity, {});
|
|
2961
3047
|
if (signedRequest.headers["X-Amz-Security-Token"] || signedRequest.headers["x-amz-security-token"]) {
|
|
2962
3048
|
throw new Error("X-Amz-Security-Token must not be set for s3-express requests.");
|
|
2963
3049
|
}
|
|
@@ -2980,15 +3066,15 @@ var s3ExpressHttpSigningMiddleware = (config) => (next, context) => async (args)
|
|
|
2980
3066
|
throw new Error(`No HttpAuthScheme was selected: unable to sign request`);
|
|
2981
3067
|
}
|
|
2982
3068
|
const { httpAuthOption: { signingProperties = {} }, identity, signer } = scheme;
|
|
2983
|
-
let
|
|
3069
|
+
let request;
|
|
2984
3070
|
if (context.s3ExpressIdentity) {
|
|
2985
|
-
|
|
3071
|
+
request = await signS3Express(context.s3ExpressIdentity, signingProperties, args.request, await config.signer());
|
|
2986
3072
|
} else {
|
|
2987
|
-
|
|
3073
|
+
request = await signer.sign(args.request, identity, signingProperties);
|
|
2988
3074
|
}
|
|
2989
3075
|
const output = await next({
|
|
2990
3076
|
...args,
|
|
2991
|
-
request
|
|
3077
|
+
request
|
|
2992
3078
|
}).catch((signer.errorHandler || defaultErrorHandler)(signingProperties));
|
|
2993
3079
|
(signer.successHandler || defaultSuccessHandler)(output.response, signingProperties);
|
|
2994
3080
|
return output;
|
|
@@ -3060,9 +3146,9 @@ var getThrow200ExceptionsPlugin = (config) => ({
|
|
|
3060
3146
|
var validate = (str) => typeof str === "string" && str.indexOf("arn:") === 0 && str.split(":").length >= 6;
|
|
3061
3147
|
|
|
3062
3148
|
// ../../node_modules/@aws-sdk/core/dist-es/submodules/util/util-format-url/format-url.js
|
|
3063
|
-
function formatUrl(
|
|
3064
|
-
const { port, query } =
|
|
3065
|
-
let { protocol, path, hostname } =
|
|
3149
|
+
function formatUrl(request) {
|
|
3150
|
+
const { port, query } = request;
|
|
3151
|
+
let { protocol, path, hostname } = request;
|
|
3066
3152
|
if (protocol && protocol.slice(-1) !== ":") {
|
|
3067
3153
|
protocol += ":";
|
|
3068
3154
|
}
|
|
@@ -3077,14 +3163,14 @@ function formatUrl(request2) {
|
|
|
3077
3163
|
queryString = `?${queryString}`;
|
|
3078
3164
|
}
|
|
3079
3165
|
let auth = "";
|
|
3080
|
-
if (
|
|
3081
|
-
const username =
|
|
3082
|
-
const password =
|
|
3166
|
+
if (request.username != null || request.password != null) {
|
|
3167
|
+
const username = request.username ?? "";
|
|
3168
|
+
const password = request.password ?? "";
|
|
3083
3169
|
auth = `${username}:${password}@`;
|
|
3084
3170
|
}
|
|
3085
3171
|
let fragment = "";
|
|
3086
|
-
if (
|
|
3087
|
-
fragment = `#${
|
|
3172
|
+
if (request.fragment) {
|
|
3173
|
+
fragment = `#${request.fragment}`;
|
|
3088
3174
|
}
|
|
3089
3175
|
return `${protocol}//${auth}${hostname}${path}${queryString}${fragment}`;
|
|
3090
3176
|
}
|
|
@@ -3477,18 +3563,18 @@ var AwsRestXmlProtocol = class extends HttpBindingProtocol {
|
|
|
3477
3563
|
return "aws.protocols#restXml";
|
|
3478
3564
|
}
|
|
3479
3565
|
async serializeRequest(operationSchema, input, context) {
|
|
3480
|
-
const
|
|
3566
|
+
const request = await super.serializeRequest(operationSchema, input, context);
|
|
3481
3567
|
const inputSchema = NormalizedSchema.of(operationSchema.input);
|
|
3482
|
-
if (!
|
|
3568
|
+
if (!request.headers["content-type"]) {
|
|
3483
3569
|
const contentType = this.mixin.resolveRestContentType(this.getDefaultContentType(), inputSchema);
|
|
3484
3570
|
if (contentType) {
|
|
3485
|
-
|
|
3571
|
+
request.headers["content-type"] = contentType;
|
|
3486
3572
|
}
|
|
3487
3573
|
}
|
|
3488
|
-
if (typeof
|
|
3489
|
-
|
|
3574
|
+
if (typeof request.body === "string" && request.headers["content-type"] === this.getDefaultContentType() && !request.body.startsWith("<?xml ") && !this.hasUnstructuredPayloadBinding(inputSchema)) {
|
|
3575
|
+
request.body = '<?xml version="1.0" encoding="UTF-8"?>' + request.body;
|
|
3490
3576
|
}
|
|
3491
|
-
return
|
|
3577
|
+
return request;
|
|
3492
3578
|
}
|
|
3493
3579
|
async deserializeResponse(operationSchema, context, response) {
|
|
3494
3580
|
return super.deserializeResponse(operationSchema, context, response);
|
|
@@ -3541,7 +3627,7 @@ var AwsRestXmlProtocol = class extends HttpBindingProtocol {
|
|
|
3541
3627
|
// ../../node_modules/@aws-sdk/middleware-sdk-s3/dist-es/submodules/s3/protocol/S3RestXmlProtocol.js
|
|
3542
3628
|
var S3RestXmlProtocol = class extends AwsRestXmlProtocol {
|
|
3543
3629
|
async serializeRequest(operationSchema, input, context) {
|
|
3544
|
-
const
|
|
3630
|
+
const request = await super.serializeRequest(operationSchema, input, context);
|
|
3545
3631
|
const ns = NormalizedSchema.of(operationSchema.input);
|
|
3546
3632
|
const staticStructureSchema = ns.getSchema();
|
|
3547
3633
|
let bucketMemberIndex = 0;
|
|
@@ -3559,7 +3645,7 @@ var S3RestXmlProtocol = class extends AwsRestXmlProtocol {
|
|
|
3559
3645
|
}
|
|
3560
3646
|
}
|
|
3561
3647
|
}
|
|
3562
|
-
return
|
|
3648
|
+
return request;
|
|
3563
3649
|
}
|
|
3564
3650
|
};
|
|
3565
3651
|
|
|
@@ -3575,12 +3661,12 @@ var NODE_USE_ARN_REGION_CONFIG_OPTIONS = {
|
|
|
3575
3661
|
// ../../node_modules/@aws-sdk/middleware-sdk-s3/dist-es/submodules/s3/middleware-expect-continue/middleware-expect-continue.js
|
|
3576
3662
|
function addExpectContinueMiddleware(options) {
|
|
3577
3663
|
return (next) => async (args) => {
|
|
3578
|
-
const { request
|
|
3579
|
-
if (options.expectContinueHeader !== false && HttpRequest.isInstance(
|
|
3664
|
+
const { request } = args;
|
|
3665
|
+
if (options.expectContinueHeader !== false && HttpRequest.isInstance(request) && request.body && options.runtime === "node" && options.requestHandler?.constructor?.name !== "FetchHttpHandler") {
|
|
3580
3666
|
let sendHeader = true;
|
|
3581
3667
|
if (typeof options.expectContinueHeader === "number") {
|
|
3582
3668
|
try {
|
|
3583
|
-
const bodyLength = Number(
|
|
3669
|
+
const bodyLength = Number(request.headers?.["content-length"]) ?? options.bodyLengthChecker?.(request.body) ?? Infinity;
|
|
3584
3670
|
sendHeader = bodyLength >= options.expectContinueHeader;
|
|
3585
3671
|
} catch (e2) {
|
|
3586
3672
|
}
|
|
@@ -3588,12 +3674,12 @@ function addExpectContinueMiddleware(options) {
|
|
|
3588
3674
|
sendHeader = !!options.expectContinueHeader;
|
|
3589
3675
|
}
|
|
3590
3676
|
if (sendHeader) {
|
|
3591
|
-
|
|
3677
|
+
request.headers.Expect = "100-continue";
|
|
3592
3678
|
}
|
|
3593
3679
|
}
|
|
3594
3680
|
return next({
|
|
3595
3681
|
...args,
|
|
3596
|
-
request
|
|
3682
|
+
request
|
|
3597
3683
|
});
|
|
3598
3684
|
};
|
|
3599
3685
|
}
|
|
@@ -11398,7 +11484,7 @@ var defaultProvider = (init = {}) => memoizeChain([
|
|
|
11398
11484
|
},
|
|
11399
11485
|
async (awsIdentityProperties) => {
|
|
11400
11486
|
init.logger?.debug("@aws-sdk/credential-provider-node - defaultProvider::fromIni");
|
|
11401
|
-
const { fromIni } = await import("./dist-es-
|
|
11487
|
+
const { fromIni } = await import("./dist-es-AXFRS7PS.js");
|
|
11402
11488
|
return fromIni(init)(awsIdentityProperties);
|
|
11403
11489
|
},
|
|
11404
11490
|
async (awsIdentityProperties) => {
|
|
@@ -11450,14 +11536,14 @@ var readableStreamHasher = (hashCtor, readableStream) => {
|
|
|
11450
11536
|
const hash = new hashCtor();
|
|
11451
11537
|
const hashCalculator = new HashCalculator(hash);
|
|
11452
11538
|
readableStream.pipe(hashCalculator);
|
|
11453
|
-
return new Promise((
|
|
11539
|
+
return new Promise((resolve2, reject) => {
|
|
11454
11540
|
readableStream.on("error", (err) => {
|
|
11455
11541
|
hashCalculator.end();
|
|
11456
11542
|
reject(err);
|
|
11457
11543
|
});
|
|
11458
11544
|
hashCalculator.on("error", reject);
|
|
11459
11545
|
hashCalculator.on("finish", () => {
|
|
11460
|
-
hash.digest().then(
|
|
11546
|
+
hash.digest().then(resolve2).catch(reject);
|
|
11461
11547
|
});
|
|
11462
11548
|
});
|
|
11463
11549
|
};
|
|
@@ -11890,8 +11976,8 @@ var BYTE_LENGTH_SOURCE;
|
|
|
11890
11976
|
BYTE_LENGTH_SOURCE2["START_END_DIFF"] = "the numeric difference between Body.start and Body.end";
|
|
11891
11977
|
BYTE_LENGTH_SOURCE2["LSTAT"] = "the size of the file given by Body.path on disk as reported by lstatSync";
|
|
11892
11978
|
})(BYTE_LENGTH_SOURCE || (BYTE_LENGTH_SOURCE = {}));
|
|
11893
|
-
var byteLengthSource = (input,
|
|
11894
|
-
if (
|
|
11979
|
+
var byteLengthSource = (input, override) => {
|
|
11980
|
+
if (override != null) {
|
|
11895
11981
|
return BYTE_LENGTH_SOURCE.CONTENT_LENGTH;
|
|
11896
11982
|
}
|
|
11897
11983
|
if (input == null) {
|
|
@@ -12187,8 +12273,8 @@ var Upload = class _Upload extends EventEmitter {
|
|
|
12187
12273
|
const requestHandler = this.client.config.requestHandler;
|
|
12188
12274
|
const eventEmitter = requestHandler instanceof EventEmitter ? requestHandler : null;
|
|
12189
12275
|
let lastSeenBytes = 0;
|
|
12190
|
-
const uploadEventListener = (event,
|
|
12191
|
-
const requestPartSize = Number(
|
|
12276
|
+
const uploadEventListener = (event, request) => {
|
|
12277
|
+
const requestPartSize = Number(request.query["partNumber"]) || -1;
|
|
12192
12278
|
if (requestPartSize !== dataPart.partNumber) {
|
|
12193
12279
|
return;
|
|
12194
12280
|
}
|
|
@@ -12313,7 +12399,7 @@ to input.params.ContentLength in bytes.
|
|
|
12313
12399
|
}
|
|
12314
12400
|
}
|
|
12315
12401
|
async __abortTimeout(abortSignal) {
|
|
12316
|
-
return new Promise((
|
|
12402
|
+
return new Promise((resolve2, reject) => {
|
|
12317
12403
|
abortSignal.onabort = () => {
|
|
12318
12404
|
const abortError = new Error("Upload aborted.");
|
|
12319
12405
|
abortError.name = "AbortError";
|
|
@@ -12426,13 +12512,13 @@ var getSignedUrl = async (client, command, options = {}) => {
|
|
|
12426
12512
|
s3Presigner = new S3RequestPresigner(client.config);
|
|
12427
12513
|
}
|
|
12428
12514
|
const presignInterceptMiddleware = (next, context) => async (args) => {
|
|
12429
|
-
const { request
|
|
12430
|
-
if (!HttpRequest.isInstance(
|
|
12515
|
+
const { request } = args;
|
|
12516
|
+
if (!HttpRequest.isInstance(request)) {
|
|
12431
12517
|
throw new Error("Request to be presigned is not an valid HTTP request.");
|
|
12432
12518
|
}
|
|
12433
|
-
delete
|
|
12434
|
-
delete
|
|
12435
|
-
delete
|
|
12519
|
+
delete request.headers["amz-sdk-invocation-id"];
|
|
12520
|
+
delete request.headers["amz-sdk-request"];
|
|
12521
|
+
delete request.headers["x-amz-user-agent"];
|
|
12436
12522
|
let presigned2;
|
|
12437
12523
|
const presignerOptions = {
|
|
12438
12524
|
...options,
|
|
@@ -12440,9 +12526,9 @@ var getSignedUrl = async (client, command, options = {}) => {
|
|
|
12440
12526
|
signingService: options.signingService ?? context["signing_service"]
|
|
12441
12527
|
};
|
|
12442
12528
|
if (context.s3ExpressIdentity) {
|
|
12443
|
-
presigned2 = await s3Presigner.presignWithCredentials(
|
|
12529
|
+
presigned2 = await s3Presigner.presignWithCredentials(request, context.s3ExpressIdentity, presignerOptions);
|
|
12444
12530
|
} else {
|
|
12445
|
-
presigned2 = await s3Presigner.presign(
|
|
12531
|
+
presigned2 = await s3Presigner.presign(request, presignerOptions);
|
|
12446
12532
|
}
|
|
12447
12533
|
return {
|
|
12448
12534
|
response: {},
|
|
@@ -12680,6 +12766,8 @@ var CAPS3 = /* @__PURE__ */ new Set([
|
|
|
12680
12766
|
"streamingPut",
|
|
12681
12767
|
"streamingGet"
|
|
12682
12768
|
]);
|
|
12769
|
+
var UPLOAD_STALL_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
12770
|
+
var UPLOAD_STALL_PROBE_MS = 30 * 1e3;
|
|
12683
12771
|
var SftpDestinationImpl = class {
|
|
12684
12772
|
kind;
|
|
12685
12773
|
capabilities = CAPS3;
|
|
@@ -12727,7 +12815,7 @@ var SftpDestinationImpl = class {
|
|
|
12727
12815
|
// ── Connection helper ────────────────────────────────────────────────
|
|
12728
12816
|
async withSftp(fn) {
|
|
12729
12817
|
const client = new Client2();
|
|
12730
|
-
return new Promise((
|
|
12818
|
+
return new Promise((resolve2, reject) => {
|
|
12731
12819
|
const cleanup = () => {
|
|
12732
12820
|
try {
|
|
12733
12821
|
client.end();
|
|
@@ -12744,7 +12832,7 @@ var SftpDestinationImpl = class {
|
|
|
12744
12832
|
fn(sftp).then(
|
|
12745
12833
|
(val) => {
|
|
12746
12834
|
cleanup();
|
|
12747
|
-
|
|
12835
|
+
resolve2(val);
|
|
12748
12836
|
},
|
|
12749
12837
|
(e2) => {
|
|
12750
12838
|
cleanup();
|
|
@@ -12764,12 +12852,12 @@ var SftpDestinationImpl = class {
|
|
|
12764
12852
|
let current = dir.startsWith("/") ? "" : ".";
|
|
12765
12853
|
for (const part of parts) {
|
|
12766
12854
|
current = current === "" ? `/${part}` : posix2.join(current, part);
|
|
12767
|
-
await new Promise((
|
|
12855
|
+
await new Promise((resolve2, reject) => {
|
|
12768
12856
|
sftp.mkdir(current, (err) => {
|
|
12769
|
-
if (!err) return
|
|
12857
|
+
if (!err) return resolve2();
|
|
12770
12858
|
sftp.stat(current, (statErr, stats) => {
|
|
12771
12859
|
if (statErr) return reject(err);
|
|
12772
|
-
if (stats.isDirectory()) return
|
|
12860
|
+
if (stats.isDirectory()) return resolve2();
|
|
12773
12861
|
reject(new Error(`${current} exists but is not a directory`));
|
|
12774
12862
|
});
|
|
12775
12863
|
});
|
|
@@ -12783,14 +12871,14 @@ var SftpDestinationImpl = class {
|
|
|
12783
12871
|
await this.withSftp(async (sftp) => {
|
|
12784
12872
|
await this.ensureDir(sftp, this.rootPath);
|
|
12785
12873
|
const probePath = posix2.join(this.rootPath, probeName);
|
|
12786
|
-
await new Promise((
|
|
12874
|
+
await new Promise((resolve2, reject) => {
|
|
12787
12875
|
const ws = sftp.createWriteStream(probePath);
|
|
12788
12876
|
ws.on("error", reject);
|
|
12789
|
-
ws.on("close", () =>
|
|
12877
|
+
ws.on("close", () => resolve2());
|
|
12790
12878
|
ws.end("ok");
|
|
12791
12879
|
});
|
|
12792
12880
|
await new Promise(
|
|
12793
|
-
(
|
|
12881
|
+
(resolve2, reject) => sftp.unlink(probePath, (err) => err ? reject(err) : resolve2())
|
|
12794
12882
|
);
|
|
12795
12883
|
});
|
|
12796
12884
|
return { ok: true };
|
|
@@ -12807,27 +12895,49 @@ var SftpDestinationImpl = class {
|
|
|
12807
12895
|
let bytesWritten = 0;
|
|
12808
12896
|
await this.withSftp(async (sftp) => {
|
|
12809
12897
|
await this.ensureDir(sftp, posix2.dirname(target));
|
|
12810
|
-
await new Promise((
|
|
12898
|
+
await new Promise((resolve2, reject) => {
|
|
12811
12899
|
const ws = sftp.createWriteStream(tmp);
|
|
12812
|
-
|
|
12813
|
-
|
|
12900
|
+
let lastProgressAt = Date.now();
|
|
12901
|
+
let settled = false;
|
|
12902
|
+
let watchdog;
|
|
12903
|
+
const finish = (err) => {
|
|
12904
|
+
if (settled) return;
|
|
12905
|
+
settled = true;
|
|
12906
|
+
if (watchdog) clearInterval(watchdog);
|
|
12907
|
+
if (err) {
|
|
12908
|
+
ws.destroy();
|
|
12909
|
+
body.destroy();
|
|
12910
|
+
reject(err);
|
|
12911
|
+
} else {
|
|
12912
|
+
resolve2();
|
|
12913
|
+
}
|
|
12914
|
+
};
|
|
12915
|
+
watchdog = setInterval(() => {
|
|
12916
|
+
if (settled || Date.now() - lastProgressAt <= UPLOAD_STALL_TIMEOUT_MS) return;
|
|
12917
|
+
finish(
|
|
12918
|
+
new Error(
|
|
12919
|
+
`SFTP upload stalled: no write progress for ${UPLOAD_STALL_TIMEOUT_MS / 1e3}s (wrote ${bytesWritten} bytes so far). Retry; if it recurs, check both ends \u2014 whether the source is still producing bytes, and the destination's SFTP subsystem.`
|
|
12920
|
+
)
|
|
12921
|
+
);
|
|
12922
|
+
}, UPLOAD_STALL_PROBE_MS);
|
|
12923
|
+
watchdog.unref?.();
|
|
12924
|
+
ws.on("error", (err) => finish(err));
|
|
12925
|
+
ws.on("close", () => finish());
|
|
12814
12926
|
body.on("data", (chunk) => {
|
|
12815
12927
|
bytesWritten += chunk.byteLength;
|
|
12928
|
+
lastProgressAt = Date.now();
|
|
12816
12929
|
});
|
|
12817
|
-
body.on("error", (err) =>
|
|
12818
|
-
ws.destroy();
|
|
12819
|
-
reject(err);
|
|
12820
|
-
});
|
|
12930
|
+
body.on("error", (err) => finish(err));
|
|
12821
12931
|
body.pipe(ws);
|
|
12822
12932
|
});
|
|
12823
|
-
await new Promise((
|
|
12933
|
+
await new Promise((resolve2, reject) => {
|
|
12824
12934
|
sftp.rename(tmp, target, (err) => {
|
|
12825
|
-
if (!err) return
|
|
12935
|
+
if (!err) return resolve2();
|
|
12826
12936
|
sftp.unlink(target, () => {
|
|
12827
12937
|
sftp.rename(
|
|
12828
12938
|
tmp,
|
|
12829
12939
|
target,
|
|
12830
|
-
(err2) => err2 ? reject(err2) :
|
|
12940
|
+
(err2) => err2 ? reject(err2) : resolve2()
|
|
12831
12941
|
);
|
|
12832
12942
|
});
|
|
12833
12943
|
});
|
|
@@ -12874,14 +12984,14 @@ var SftpDestinationImpl = class {
|
|
|
12874
12984
|
const target = this.fullPath(key);
|
|
12875
12985
|
try {
|
|
12876
12986
|
return await this.withSftp(
|
|
12877
|
-
(sftp) => new Promise((
|
|
12987
|
+
(sftp) => new Promise((resolve2, reject) => {
|
|
12878
12988
|
sftp.stat(target, (err, stats) => {
|
|
12879
12989
|
if (err) {
|
|
12880
12990
|
const code = err.code;
|
|
12881
|
-
if (code === 2) return
|
|
12991
|
+
if (code === 2) return resolve2(null);
|
|
12882
12992
|
return reject(err);
|
|
12883
12993
|
}
|
|
12884
|
-
|
|
12994
|
+
resolve2({
|
|
12885
12995
|
sizeBytes: stats.size,
|
|
12886
12996
|
uploadedAt: new Date(stats.mtime * 1e3)
|
|
12887
12997
|
});
|
|
@@ -12898,14 +13008,14 @@ var SftpDestinationImpl = class {
|
|
|
12898
13008
|
const entries = [];
|
|
12899
13009
|
await this.withSftp(async (sftp) => {
|
|
12900
13010
|
const walk = async (dir, relBase) => {
|
|
12901
|
-
const dirents = await new Promise((
|
|
13011
|
+
const dirents = await new Promise((resolve2, reject) => {
|
|
12902
13012
|
sftp.readdir(dir, (err, list) => {
|
|
12903
13013
|
if (err) {
|
|
12904
13014
|
const code = err.code;
|
|
12905
|
-
if (code === 2) return
|
|
13015
|
+
if (code === 2) return resolve2([]);
|
|
12906
13016
|
return reject(err);
|
|
12907
13017
|
}
|
|
12908
|
-
|
|
13018
|
+
resolve2(list);
|
|
12909
13019
|
});
|
|
12910
13020
|
});
|
|
12911
13021
|
for (const dirent of dirents) {
|
|
@@ -12931,10 +13041,10 @@ var SftpDestinationImpl = class {
|
|
|
12931
13041
|
const target = this.fullPath(key);
|
|
12932
13042
|
await this.withSftp(
|
|
12933
13043
|
(sftp) => new Promise(
|
|
12934
|
-
(
|
|
12935
|
-
if (!err) return
|
|
13044
|
+
(resolve2, reject) => sftp.unlink(target, (err) => {
|
|
13045
|
+
if (!err) return resolve2();
|
|
12936
13046
|
const code = err.code;
|
|
12937
|
-
if (code === 2) return
|
|
13047
|
+
if (code === 2) return resolve2();
|
|
12938
13048
|
reject(err);
|
|
12939
13049
|
})
|
|
12940
13050
|
)
|
|
@@ -12946,11 +13056,11 @@ var SftpDestinationImpl = class {
|
|
|
12946
13056
|
const failed = [];
|
|
12947
13057
|
await this.withSftp(async (sftp) => {
|
|
12948
13058
|
for (const key of keys) {
|
|
12949
|
-
await new Promise((
|
|
13059
|
+
await new Promise((resolve2) => {
|
|
12950
13060
|
sftp.unlink(this.fullPath(key), (err) => {
|
|
12951
13061
|
if (!err) {
|
|
12952
13062
|
deleted.push(key);
|
|
12953
|
-
return
|
|
13063
|
+
return resolve2();
|
|
12954
13064
|
}
|
|
12955
13065
|
const code = err.code;
|
|
12956
13066
|
if (code === 2) {
|
|
@@ -12958,7 +13068,7 @@ var SftpDestinationImpl = class {
|
|
|
12958
13068
|
} else {
|
|
12959
13069
|
failed.push({ key, error: err.message });
|
|
12960
13070
|
}
|
|
12961
|
-
|
|
13071
|
+
resolve2();
|
|
12962
13072
|
});
|
|
12963
13073
|
});
|
|
12964
13074
|
}
|
|
@@ -12969,1311 +13079,52 @@ var SftpDestinationImpl = class {
|
|
|
12969
13079
|
registerDestination("sftp", (row) => new SftpDestinationImpl(row));
|
|
12970
13080
|
registerDestination("openship_server", (row) => new SftpDestinationImpl(row));
|
|
12971
13081
|
|
|
12972
|
-
// src/lib/
|
|
12973
|
-
|
|
12974
|
-
|
|
12975
|
-
import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
12976
|
-
import { homedir as homedir2, userInfo } from "os";
|
|
12977
|
-
import { join as join7 } from "path";
|
|
12978
|
-
|
|
12979
|
-
// src/lib/paths.ts
|
|
12980
|
-
import { homedir } from "os";
|
|
12981
|
-
import { join as join3, resolve as resolve2 } from "path";
|
|
12982
|
-
var DEFAULT_HOME = join3(homedir(), ".openship");
|
|
12983
|
-
var override = process.env.OPENSHIP_HOME?.trim();
|
|
12984
|
-
var OS_DIR = override ? resolve2(override) : DEFAULT_HOME;
|
|
12985
|
-
var IS_ALT_HOME = OS_DIR !== DEFAULT_HOME;
|
|
12986
|
-
var DATA_DIR = join3(OS_DIR, "data");
|
|
12987
|
-
var LOG_DIR = join3(OS_DIR, "logs");
|
|
12988
|
-
var AUTH_SECRET_FILE = join3(OS_DIR, "auth-secret");
|
|
12989
|
-
var INTERNAL_TOKEN_FILE = join3(OS_DIR, "internal-token");
|
|
12990
|
-
|
|
12991
|
-
// src/lib/ports.ts
|
|
12992
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
12993
|
-
import { join as join4 } from "path";
|
|
12994
|
-
|
|
12995
|
-
// ../../packages/core/src/ports.ts
|
|
12996
|
-
import { createServer } from "net";
|
|
12997
|
-
var DEFAULT_API_PORT = 4e3;
|
|
12998
|
-
var DEFAULT_DASHBOARD_PORT = 3001;
|
|
12999
|
-
var LOOPBACK = "127.0.0.1";
|
|
13000
|
-
var SCAN_SPAN = 20;
|
|
13001
|
-
var MAX_PORT = 65535;
|
|
13002
|
-
function isPortFree(port, host = LOOPBACK) {
|
|
13003
|
-
return new Promise((resolve4) => {
|
|
13004
|
-
const srv = createServer();
|
|
13005
|
-
srv.once("error", () => resolve4(false));
|
|
13006
|
-
srv.listen(port, host, () => srv.close(() => resolve4(true)));
|
|
13007
|
-
});
|
|
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
|
-
}
|
|
13160
|
-
|
|
13161
|
-
// src/lib/source-install.ts
|
|
13162
|
-
import { spawnSync as spawnSync2 } from "child_process";
|
|
13163
|
-
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
13164
|
-
import { join as join6 } from "path";
|
|
13165
|
-
|
|
13166
|
-
// src/lib/from-source.ts
|
|
13167
|
-
import { spawn, spawnSync } from "child_process";
|
|
13168
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
13169
|
-
import { join as join5, resolve as resolve3 } from "path";
|
|
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");
|
|
13173
|
-
function has(cmd) {
|
|
13174
|
-
try {
|
|
13175
|
-
return spawnSync(cmd, ["--version"], { stdio: "ignore" }).status === 0;
|
|
13176
|
-
} catch {
|
|
13177
|
-
return false;
|
|
13178
|
-
}
|
|
13179
|
-
}
|
|
13180
|
-
function run(cmd, args, cwd, env) {
|
|
13181
|
-
return new Promise((res, rej) => {
|
|
13182
|
-
const child = spawn(cmd, args, {
|
|
13183
|
-
cwd,
|
|
13184
|
-
stdio: "inherit",
|
|
13185
|
-
// Windows resolves the bun/git shims (.cmd/.exe) only through a shell.
|
|
13186
|
-
shell: process.platform === "win32",
|
|
13187
|
-
env: env ? { ...process.env, ...env } : process.env
|
|
13188
|
-
});
|
|
13189
|
-
child.on("error", rej);
|
|
13190
|
-
child.on(
|
|
13191
|
-
"exit",
|
|
13192
|
-
(code) => code === 0 ? res() : rej(new Error(`\`${cmd} ${args.join(" ")}\` (cwd=${cwd}) exited ${code ?? "?"}`))
|
|
13193
|
-
);
|
|
13194
|
-
});
|
|
13195
|
-
}
|
|
13196
|
-
function shortSha(cwd) {
|
|
13197
|
-
const r2 = spawnSync("git", ["rev-parse", "--short", "HEAD"], { cwd, encoding: "utf8" });
|
|
13198
|
-
return r2.status === 0 ? (r2.stdout ?? "").trim() || "unknown" : "unknown";
|
|
13199
|
-
}
|
|
13200
|
-
function isMonorepo(dir) {
|
|
13201
|
-
return existsSync2(join5(dir, "package.json")) && existsSync2(join5(dir, "apps/api/package.json")) && existsSync2(join5(dir, "apps/dashboard/package.json"));
|
|
13202
|
-
}
|
|
13203
|
-
async function prepareFromSource(opts) {
|
|
13204
|
-
if (!has("bun")) {
|
|
13205
|
-
throw new Error(
|
|
13206
|
-
"`bun` is required to build from source but wasn't found on PATH \u2014 install it: https://bun.sh"
|
|
13207
|
-
);
|
|
13208
|
-
}
|
|
13209
|
-
let sourceDir;
|
|
13210
|
-
let ref;
|
|
13211
|
-
if (opts.source) {
|
|
13212
|
-
sourceDir = resolve3(opts.source);
|
|
13213
|
-
if (!isMonorepo(sourceDir)) {
|
|
13214
|
-
throw new Error(
|
|
13215
|
-
`--source ${sourceDir} doesn't look like an Openship checkout (missing package.json / apps/api / apps/dashboard).`
|
|
13216
|
-
);
|
|
13217
|
-
}
|
|
13218
|
-
ref = "local";
|
|
13219
|
-
} else {
|
|
13220
|
-
if (!has("git")) {
|
|
13221
|
-
throw new Error("`git` is required to clone the source but wasn't found on PATH.");
|
|
13222
|
-
}
|
|
13223
|
-
ref = (opts.ref || "main").trim();
|
|
13224
|
-
const repoUrl = opts.repo || DEFAULT_REPO;
|
|
13225
|
-
sourceDir = SOURCE_CHECKOUT_DIR;
|
|
13226
|
-
mkdirSync2(OS_DIR, { recursive: true });
|
|
13227
|
-
if (!existsSync2(join5(sourceDir, ".git"))) {
|
|
13228
|
-
console.log(` Cloning ${repoUrl} \u2192 ${sourceDir}`);
|
|
13229
|
-
await run("git", ["clone", repoUrl, sourceDir], OS_DIR);
|
|
13230
|
-
}
|
|
13231
|
-
console.log(` Fetching + checking out ${ref}`);
|
|
13232
|
-
await run("git", ["fetch", "origin", ref, "--tags"], sourceDir);
|
|
13233
|
-
await run("git", ["checkout", ref], sourceDir);
|
|
13234
|
-
await run("git", ["pull", "--ff-only", "origin", ref], sourceDir).catch(() => {
|
|
13235
|
-
console.log(" (pinned ref \u2014 not fast-forwarding)");
|
|
13236
|
-
});
|
|
13237
|
-
}
|
|
13238
|
-
const sha = shortSha(sourceDir);
|
|
13239
|
-
console.log(` Source: ${sourceDir} @ ${ref} (${sha})`);
|
|
13240
|
-
console.log(" Installing workspace dependencies (bun install)\u2026");
|
|
13241
|
-
await run("bun", ["install"], sourceDir);
|
|
13242
|
-
const distDir = SOURCE_DIST_DIR;
|
|
13243
|
-
console.log(" Building release dist (compiles the dashboard \u2014 needs RAM/CPU)\u2026");
|
|
13244
|
-
await run(
|
|
13245
|
-
"bun",
|
|
13246
|
-
["run", join5(sourceDir, "apps/api/scripts/build-release.ts")],
|
|
13247
|
-
sourceDir,
|
|
13248
|
-
{ DIST_DIR: distDir, NODE_ENV: "production", CLOUD_MODE: "false", OPENSHIP_TARGET: "local" }
|
|
13249
|
-
);
|
|
13250
|
-
console.log(" Installing runtime dependencies in the dist\u2026");
|
|
13251
|
-
await run("bun", ["install", "--production", "--frozen-lockfile"], distDir);
|
|
13252
|
-
const apiDir = join5(distDir, "api");
|
|
13253
|
-
const dashboardDir = join5(distDir, "dashboard");
|
|
13254
|
-
if (!existsSync2(join5(apiDir, "src/index.ts"))) {
|
|
13255
|
-
throw new Error(`Build produced no API at ${apiDir}/src/index.ts \u2014 build-release layout drift?`);
|
|
13256
|
-
}
|
|
13257
|
-
if (!existsSync2(join5(dashboardDir, "apps/dashboard/server.js"))) {
|
|
13258
|
-
throw new Error(`Build produced no dashboard at ${dashboardDir}/apps/dashboard/server.js.`);
|
|
13259
|
-
}
|
|
13260
|
-
return { apiDir, dashboardDir, ref, sha, sourceDir };
|
|
13261
|
-
}
|
|
13262
|
-
|
|
13263
|
-
// src/lib/source-install.ts
|
|
13264
|
-
var MARKER = join6(OS_DIR, "source-install.json");
|
|
13265
|
-
function parseSourceInstall(raw) {
|
|
13266
|
-
try {
|
|
13267
|
-
const obj = JSON.parse(raw);
|
|
13268
|
-
if (!obj || typeof obj.dir !== "string" || !obj.dir) return null;
|
|
13269
|
-
return { repo: obj.repo || DEFAULT_REPO, ref: obj.ref || "main", dir: obj.dir };
|
|
13270
|
-
} catch {
|
|
13271
|
-
return null;
|
|
13272
|
-
}
|
|
13273
|
-
}
|
|
13274
|
-
function readSourceInstall() {
|
|
13275
|
-
if (!existsSync3(MARKER)) return null;
|
|
13276
|
-
try {
|
|
13277
|
-
return parseSourceInstall(readFileSync2(MARKER, "utf8"));
|
|
13278
|
-
} catch {
|
|
13279
|
-
return null;
|
|
13280
|
-
}
|
|
13082
|
+
// src/lib/this-host.ts
|
|
13083
|
+
function thisHost() {
|
|
13084
|
+
return envOps(resolveLocalEnvironmentSync());
|
|
13281
13085
|
}
|
|
13282
|
-
function
|
|
13283
|
-
|
|
13284
|
-
|
|
13086
|
+
function pasteable(op) {
|
|
13087
|
+
if (!op.supported) return null;
|
|
13088
|
+
const sudo = thisHost().profile.isRoot ? "" : "sudo ";
|
|
13089
|
+
return op.value.map((step) => `${sudo}${step}`).join(" && ");
|
|
13285
13090
|
}
|
|
13286
|
-
function
|
|
13287
|
-
|
|
13288
|
-
if (r2.status !== 0 || !r2.stdout) return null;
|
|
13289
|
-
return parseRemoteSha(r2.stdout);
|
|
13290
|
-
}
|
|
13291
|
-
async function rebuildFromSource(info) {
|
|
13292
|
-
if (!has("bun")) {
|
|
13293
|
-
throw new Error("`bun` is required to rebuild from source but wasn't found on PATH \u2014 https://bun.sh");
|
|
13294
|
-
}
|
|
13295
|
-
if (!has("git")) {
|
|
13296
|
-
throw new Error("`git` is required to update the source checkout but wasn't found on PATH.");
|
|
13297
|
-
}
|
|
13298
|
-
if (!existsSync3(join6(info.dir, ".git"))) {
|
|
13299
|
-
throw new Error(
|
|
13300
|
-
`Source checkout ${info.dir} is missing (no .git) \u2014 reinstall from source with scripts/install-source.sh.`
|
|
13301
|
-
);
|
|
13302
|
-
}
|
|
13303
|
-
await run("git", ["fetch", "origin", info.ref, "--tags"], info.dir);
|
|
13304
|
-
await run("git", ["checkout", info.ref], info.dir);
|
|
13305
|
-
await run("git", ["pull", "--ff-only", "origin", info.ref], info.dir).catch(() => {
|
|
13306
|
-
});
|
|
13307
|
-
const cliDir = join6(info.dir, "apps/cli");
|
|
13308
|
-
await run("bun", ["install"], info.dir);
|
|
13309
|
-
await run("bun", ["run", "build"], cliDir);
|
|
13310
|
-
await run("bun", ["run", "build/stage-dashboard.ts"], cliDir);
|
|
13311
|
-
return shortSha(info.dir);
|
|
13091
|
+
function startUnitHint(unit) {
|
|
13092
|
+
return pasteable(thisHost().serviceEnableStart(unit));
|
|
13312
13093
|
}
|
|
13313
13094
|
|
|
13314
|
-
// src/lib/compose.ts
|
|
13315
|
-
|
|
13316
|
-
|
|
13317
|
-
|
|
13318
|
-
|
|
13095
|
+
// src/lib/compose-env.ts
|
|
13096
|
+
import { existsSync, readFileSync } from "fs";
|
|
13097
|
+
import { join as join3 } from "path";
|
|
13098
|
+
var COMPOSE_DIR = join3(OS_DIR, "compose");
|
|
13099
|
+
var COMPOSE_FILE = join3(COMPOSE_DIR, "docker-compose.yml");
|
|
13100
|
+
var COMPOSE_ENV_FILE = join3(COMPOSE_DIR, ".env");
|
|
13101
|
+
function composeInstallExists() {
|
|
13102
|
+
return existsSync(COMPOSE_FILE);
|
|
13319
13103
|
}
|
|
13320
|
-
|
|
13321
|
-
|
|
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");
|
|
13325
|
-
var BUILT_SERVICES = [
|
|
13326
|
-
{ service: "api", dockerfile: "apps/api/Dockerfile" },
|
|
13327
|
-
{ service: "dashboard", dockerfile: "apps/dashboard/Dockerfile" },
|
|
13328
|
-
{ service: "edge", dockerfile: "apps/edge/Dockerfile" }
|
|
13329
|
-
];
|
|
13330
|
-
var ENV_CONSUMING_SERVICES = ["api", "dashboard", "edge"];
|
|
13331
|
-
function readInstallMethod() {
|
|
13104
|
+
function readComposeEnvFile() {
|
|
13105
|
+
let text;
|
|
13332
13106
|
try {
|
|
13333
|
-
|
|
13334
|
-
|
|
13335
|
-
|
|
13336
|
-
return null;
|
|
13337
|
-
}
|
|
13338
|
-
}
|
|
13339
|
-
function writeInstallMethod(method) {
|
|
13340
|
-
mkdirSync3(OS_DIR, { recursive: true, mode: 448 });
|
|
13341
|
-
writeFileSync2(INSTALL_METHOD_FILE, method, { mode: 384 });
|
|
13342
|
-
}
|
|
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"]) };
|
|
13347
|
-
}
|
|
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
|
-
};
|
|
13107
|
+
text = readFileSync(COMPOSE_ENV_FILE, "utf8");
|
|
13108
|
+
} catch (err) {
|
|
13109
|
+
const code = err.code;
|
|
13110
|
+
return { env: {}, unreadable: code === "ENOENT" ? null : err.message };
|
|
13367
13111
|
}
|
|
13368
|
-
|
|
13369
|
-
|
|
13370
|
-
|
|
13371
|
-
|
|
13372
|
-
}
|
|
13373
|
-
function hasCmd(cmd) {
|
|
13374
|
-
return spawnSync3(cmd, ["--version"], { stdio: "ignore" }).status === 0;
|
|
13375
|
-
}
|
|
13376
|
-
function shQuote(s2) {
|
|
13377
|
-
return `'${s2.replace(/'/g, `'\\''`)}'`;
|
|
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
|
-
}
|
|
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;
|
|
13408
|
-
const asRoot = typeof process.getuid === "function" && process.getuid() === 0;
|
|
13409
|
-
const sudo = !asRoot && hasCmd("sudo") ? "sudo " : "";
|
|
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;
|
|
13440
|
-
}
|
|
13441
|
-
var COMPOSE_YAML = `# Managed by \`openship up\` \u2014 do not edit; re-run \`openship up\` to regenerate.
|
|
13442
|
-
services:
|
|
13443
|
-
postgres:
|
|
13444
|
-
image: postgres:16-alpine
|
|
13445
|
-
restart: unless-stopped
|
|
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}
|
|
13453
|
-
POSTGRES_USER: \${POSTGRES_USER:-openship}
|
|
13454
|
-
POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:?missing from .env \u2014 re-run openship up to regenerate it}
|
|
13455
|
-
POSTGRES_DB: \${POSTGRES_DB:-openship}
|
|
13456
|
-
expose: ["5432"]
|
|
13457
|
-
volumes: [postgres_data:/var/lib/postgresql/data]
|
|
13458
|
-
healthcheck:
|
|
13459
|
-
test: ["CMD-SHELL", "pg_isready -U \${POSTGRES_USER:-openship} -d \${POSTGRES_DB:-openship}"]
|
|
13460
|
-
interval: 5s
|
|
13461
|
-
timeout: 3s
|
|
13462
|
-
retries: 12
|
|
13463
|
-
|
|
13464
|
-
redis:
|
|
13465
|
-
image: redis:7-alpine
|
|
13466
|
-
restart: unless-stopped
|
|
13467
|
-
command: ["redis-server", "--appendonly", "yes"]
|
|
13468
|
-
expose: ["6379"]
|
|
13469
|
-
volumes: [redis_data:/data]
|
|
13470
|
-
healthcheck:
|
|
13471
|
-
test: ["CMD", "redis-cli", "ping"]
|
|
13472
|
-
interval: 5s
|
|
13473
|
-
timeout: 3s
|
|
13474
|
-
retries: 12
|
|
13475
|
-
|
|
13476
|
-
api:
|
|
13477
|
-
image: \${OPENSHIP_IMAGE_REGISTRY:-ghcr.io/oblien}/openship-api:\${OPENSHIP_VERSION:-latest}
|
|
13478
|
-
restart: unless-stopped
|
|
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}"]
|
|
13483
|
-
volumes:
|
|
13484
|
-
- /var/run/docker.sock:/var/run/docker.sock
|
|
13485
|
-
# Routing state shared with the edge, as HOST BIND MOUNTS (generated from
|
|
13486
|
-
# EDGE_CONTAINER_MOUNTS): the vhost tree, /etc/letsencrypt, the ACME webroot
|
|
13487
|
-
# and the static doc-roots the API writes and the edge serves. Named volumes
|
|
13488
|
-
# hid all of it from every host-side reader (migrate's proxy scan, cert carry,
|
|
13489
|
-
# cert reuse, mail cert symlinks), each of which then silently saw "nothing".
|
|
13490
|
-
${edgeVolumeYaml(" ")}
|
|
13491
|
-
# Host-op SSH key (createHostExecutor \u2192 host.docker.internal). /dev/null
|
|
13492
|
-
# when the host channel isn't provisioned \u2192 OPENSHIP_HOST_SSH_HOST stays
|
|
13493
|
-
# unset and the API falls back to LocalExecutor.
|
|
13494
|
-
- \${OPENSHIP_HOST_KEY_PATH:-/dev/null}:/run/secrets/openship_host_key:ro
|
|
13495
|
-
extra_hosts: ["host.docker.internal:host-gateway"]
|
|
13496
|
-
env_file: [.env]
|
|
13497
|
-
environment:
|
|
13498
|
-
NODE_ENV: production
|
|
13499
|
-
PORT: "\${API_PORT:-4000}"
|
|
13500
|
-
DATABASE_URL: postgresql://\${POSTGRES_USER:-openship}:\${POSTGRES_PASSWORD:?missing from .env \u2014 re-run openship up to regenerate it}@postgres:5432/\${POSTGRES_DB:-openship}
|
|
13501
|
-
REDIS_URL: redis://redis:6379
|
|
13502
|
-
OPENSHIP_EDGE_MODE: docker
|
|
13503
|
-
OPENSHIP_EDGE_CONTAINER: openship-edge
|
|
13504
|
-
depends_on:
|
|
13505
|
-
postgres: { condition: service_healthy }
|
|
13506
|
-
redis: { condition: service_healthy }
|
|
13507
|
-
healthcheck:
|
|
13508
|
-
test: ["CMD-SHELL", "bun -e \\"fetch('http://127.0.0.1:\${API_PORT:-4000}/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\\""]
|
|
13509
|
-
interval: 10s
|
|
13510
|
-
timeout: 5s
|
|
13511
|
-
retries: 12
|
|
13512
|
-
start_period: 40s
|
|
13513
|
-
|
|
13514
|
-
dashboard:
|
|
13515
|
-
image: \${OPENSHIP_IMAGE_REGISTRY:-ghcr.io/oblien}/openship-dashboard:\${OPENSHIP_VERSION:-latest}
|
|
13516
|
-
restart: unless-stopped
|
|
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}"]
|
|
13519
|
-
env_file: [.env]
|
|
13520
|
-
environment:
|
|
13521
|
-
NODE_ENV: production
|
|
13522
|
-
PORT: "\${DASHBOARD_PORT:-3001}"
|
|
13523
|
-
INTERNAL_API_URL: http://api:\${API_PORT:-4000}
|
|
13524
|
-
depends_on:
|
|
13525
|
-
api: { condition: service_healthy }
|
|
13526
|
-
|
|
13527
|
-
edge:
|
|
13528
|
-
image: \${OPENSHIP_IMAGE_REGISTRY:-ghcr.io/oblien}/openship-edge:\${OPENSHIP_VERSION:-latest}
|
|
13529
|
-
# PINNED, and it must stay pinned: the api reaches the edge by NAME through
|
|
13530
|
-
# DockerEdgeExecutor (OPENSHIP_EDGE_CONTAINER above), and "ours" edge
|
|
13531
|
-
# detection greps \`docker ps --filter name=openship-edge\`. Without this,
|
|
13532
|
-
# compose derives \`<project>-edge-1\` from the directory and every
|
|
13533
|
-
# \`docker exec\` into the edge fails with "No such container: openship-edge" \u2014
|
|
13534
|
-
# which silently migrated 0 sites after the operator's proxy was stopped.
|
|
13535
|
-
# Safe here: the edge is a singleton (host networking, one per box).
|
|
13536
|
-
container_name: openship-edge
|
|
13537
|
-
restart: unless-stopped
|
|
13538
|
-
network_mode: host
|
|
13539
|
-
volumes:
|
|
13540
|
-
${edgeVolumeYaml(" ")}
|
|
13541
|
-
|
|
13542
|
-
volumes:
|
|
13543
|
-
postgres_data:
|
|
13544
|
-
redis_data:
|
|
13545
|
-
`;
|
|
13546
|
-
function keepSecret(existing, key) {
|
|
13547
|
-
return existing[key] || randomBytes5(32).toString("hex");
|
|
13548
|
-
}
|
|
13549
|
-
function readEnvFile() {
|
|
13550
|
-
const out = {};
|
|
13551
|
-
try {
|
|
13552
|
-
for (const line of readFileSync3(ENV_FILE, "utf8").split("\n")) {
|
|
13553
|
-
const m2 = line.match(/^([A-Z0-9_]+)=(.*)$/);
|
|
13554
|
-
if (m2) out[m2[1]] = m2[2];
|
|
13555
|
-
}
|
|
13556
|
-
} catch {
|
|
13557
|
-
}
|
|
13558
|
-
return out;
|
|
13559
|
-
}
|
|
13560
|
-
var HOST_KEY_COMMENT = "openship-host-executor";
|
|
13561
|
-
var HOST_KEY_FROM = "172.16.0.0/12,192.168.0.0/16,10.0.0.0/8,127.0.0.1";
|
|
13562
|
-
function hostKeyAuthLine(pub) {
|
|
13563
|
-
return `from="${HOST_KEY_FROM}",restrict,pty ${pub}`;
|
|
13564
|
-
}
|
|
13565
|
-
function rewriteHostAuthorizedKeys(existing, pub) {
|
|
13566
|
-
const kept = existing.split("\n").filter((line) => line.trim() && !line.includes(HOST_KEY_COMMENT));
|
|
13567
|
-
return [...kept, hostKeyAuthLine(pub)].join("\n") + "\n";
|
|
13568
|
-
}
|
|
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;
|
|
13583
|
-
try {
|
|
13584
|
-
mkdirSync3(join7(COMPOSE_DIR, "host-ssh"), { recursive: true, mode: 448 });
|
|
13585
|
-
if (!existsSync4(keyPath)) {
|
|
13586
|
-
const g2 = spawnSync3(
|
|
13587
|
-
"ssh-keygen",
|
|
13588
|
-
["-t", "ed25519", "-N", "", "-q", "-f", keyPath, "-C", HOST_KEY_COMMENT],
|
|
13589
|
-
{ stdio: "ignore" }
|
|
13590
|
-
);
|
|
13591
|
-
if (g2.status !== 0) return null;
|
|
13592
|
-
}
|
|
13593
|
-
const pub = readFileSync3(`${keyPath}.pub`, "utf8").trim();
|
|
13594
|
-
if (!pub) return null;
|
|
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") : "";
|
|
13599
|
-
const next = rewriteHostAuthorizedKeys(existing, pub);
|
|
13600
|
-
if (next !== existing) writeFileSync2(authKeys, next, { mode: 384 });
|
|
13601
|
-
chmodSync(authKeys, 384);
|
|
13602
|
-
return { user, keyPath };
|
|
13603
|
-
} catch {
|
|
13604
|
-
return null;
|
|
13605
|
-
}
|
|
13606
|
-
}
|
|
13607
|
-
function composeProjectName(prev) {
|
|
13608
|
-
if (prev.COMPOSE_PROJECT_NAME) return prev.COMPOSE_PROJECT_NAME;
|
|
13609
|
-
return Object.keys(prev).length > 0 ? "compose" : "openship";
|
|
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
|
-
}
|
|
13616
|
-
function orphanedStackContainers(project) {
|
|
13617
|
-
const r2 = spawnSync3(
|
|
13618
|
-
"docker",
|
|
13619
|
-
[
|
|
13620
|
-
"ps",
|
|
13621
|
-
"-a",
|
|
13622
|
-
"--format",
|
|
13623
|
-
'{{.Names}} {{.Label "com.docker.compose.project"}} {{.Label "com.docker.compose.project.config_files"}}'
|
|
13624
|
-
],
|
|
13625
|
-
{ encoding: "utf8" }
|
|
13626
|
-
);
|
|
13627
|
-
if (r2.status !== 0 || !r2.stdout) return [];
|
|
13628
|
-
return r2.stdout.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
13629
|
-
const [name, proj, configFiles] = line.split(" ");
|
|
13630
|
-
return { name: name ?? "", project: proj ?? "", configFiles: configFiles ?? "" };
|
|
13631
|
-
}).filter(
|
|
13632
|
-
(c2) => c2.name && c2.project && c2.project !== project && // config_files is a comma-separated list (base + build override).
|
|
13633
|
-
c2.configFiles.split(",").some((f2) => f2.trim() === COMPOSE_FILE)
|
|
13634
|
-
).map(({ name, project: proj }) => ({ name, project: proj }));
|
|
13635
|
-
}
|
|
13636
|
-
function reconcileDbPassword(user, password) {
|
|
13637
|
-
if (!password) return;
|
|
13638
|
-
if (compose(["up", "-d", "--wait", "postgres"], { quiet: true }) !== 0) return;
|
|
13639
|
-
const attempt = (asRole) => spawnSync3(
|
|
13640
|
-
"docker",
|
|
13641
|
-
[
|
|
13642
|
-
"compose",
|
|
13643
|
-
"-f",
|
|
13644
|
-
COMPOSE_FILE,
|
|
13645
|
-
"exec",
|
|
13646
|
-
"-T",
|
|
13647
|
-
"postgres",
|
|
13648
|
-
"psql",
|
|
13649
|
-
"-U",
|
|
13650
|
-
asRole,
|
|
13651
|
-
"-d",
|
|
13652
|
-
"postgres",
|
|
13653
|
-
"-v",
|
|
13654
|
-
"ON_ERROR_STOP=1",
|
|
13655
|
-
// Read the statement from STDIN, not `-c`: psql has no bind parameters for
|
|
13656
|
-
// ALTER USER, so the password has to be inlined in SQL — and an argv copy
|
|
13657
|
-
// would be readable in `ps` for the life of the call. stdin keeps it to this
|
|
13658
|
-
// process and the socket. Our generated password is hex, so it cannot break
|
|
13659
|
-
// out of the quoted literal either.
|
|
13660
|
-
"-f",
|
|
13661
|
-
"-"
|
|
13662
|
-
],
|
|
13663
|
-
{
|
|
13664
|
-
cwd: COMPOSE_DIR,
|
|
13665
|
-
encoding: "utf8",
|
|
13666
|
-
// Create the role if the volume predates it, else just realign it. Both
|
|
13667
|
-
// branches leave exactly the credentials this `.env` will present.
|
|
13668
|
-
input: `DO $$ BEGIN
|
|
13669
|
-
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${user}') THEN
|
|
13670
|
-
ALTER ROLE "${user}" WITH LOGIN PASSWORD '${password}';
|
|
13671
|
-
ELSE
|
|
13672
|
-
CREATE ROLE "${user}" WITH LOGIN SUPERUSER PASSWORD '${password}';
|
|
13673
|
-
END IF;
|
|
13674
|
-
END $$;
|
|
13675
|
-
`
|
|
13676
|
-
}
|
|
13677
|
-
);
|
|
13678
|
-
let r2 = attempt(user);
|
|
13679
|
-
if (r2.status !== 0) r2 = attempt("postgres");
|
|
13680
|
-
if (r2.status !== 0) {
|
|
13681
|
-
console.log(
|
|
13682
|
-
`
|
|
13683
|
-
! Could not realign the database password (${(r2.stderr ?? "").trim() || "psql failed"}).
|
|
13684
|
-
The api will fail with "password authentication failed for user "${user}"" because the
|
|
13685
|
-
existing data volume was initialized with a different password.
|
|
13686
|
-
|
|
13687
|
-
Fix it one of two ways:
|
|
13688
|
-
\u2022 restore the .env that created the volume, or
|
|
13689
|
-
\u2022 start fresh (DESTROYS DB DATA): openship uninstall then openship up
|
|
13690
|
-
`
|
|
13691
|
-
);
|
|
13692
|
-
}
|
|
13693
|
-
}
|
|
13694
|
-
function removeOrphanedStack(project) {
|
|
13695
|
-
const orphans = orphanedStackContainers(project);
|
|
13696
|
-
if (orphans.length === 0) return;
|
|
13697
|
-
const projects = [...new Set(orphans.map((o2) => o2.project))].join(", ");
|
|
13698
|
-
console.log(
|
|
13699
|
-
` Removing ${orphans.length} container(s) from a previous stack (project "${projects}") \u2014 they would hold the ports this stack needs. Volumes are kept.`
|
|
13700
|
-
);
|
|
13701
|
-
spawnSync3("docker", ["rm", "-f", ...orphans.map((o2) => o2.name)], { stdio: "ignore" });
|
|
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
|
-
}
|
|
13722
|
-
function migrateLegacyEdgeVolumes(project) {
|
|
13723
|
-
const pairs = [
|
|
13724
|
-
{ volume: `${project}_openship_sites`, host: EDGE_SITES_HOST_DIR },
|
|
13725
|
-
{ volume: `${project}_openship_certs`, host: "/etc/letsencrypt" },
|
|
13726
|
-
{ volume: `${project}_openship_acme`, host: EDGE_ACME_HOST_DIR },
|
|
13727
|
-
{ volume: `${project}_openship_static`, host: "/opt/openship/static" }
|
|
13728
|
-
];
|
|
13729
|
-
const ls = spawnSync3("docker", ["volume", "ls", "--format", "{{.Name}}"], { encoding: "utf8" });
|
|
13730
|
-
if (ls.status !== 0 || !ls.stdout) return;
|
|
13731
|
-
const existing = new Set(ls.stdout.split("\n").map((l2) => l2.trim()).filter(Boolean));
|
|
13732
|
-
for (const { volume, host } of pairs) {
|
|
13733
|
-
if (!existing.has(volume)) continue;
|
|
13734
|
-
mkdirSync3(host, { recursive: true });
|
|
13735
|
-
const alreadyThere = spawnSync3("sh", ["-c", `ls -A ${JSON.stringify(host)} 2>/dev/null | head -1`], {
|
|
13736
|
-
encoding: "utf8"
|
|
13737
|
-
});
|
|
13738
|
-
if (alreadyThere.stdout?.trim()) continue;
|
|
13739
|
-
console.log(` Moving edge state from volume ${volume} onto ${host}...`);
|
|
13740
|
-
const copy = spawnSync3(
|
|
13741
|
-
"docker",
|
|
13742
|
-
[
|
|
13743
|
-
"run",
|
|
13744
|
-
"--rm",
|
|
13745
|
-
"-v",
|
|
13746
|
-
`${volume}:/from:ro`,
|
|
13747
|
-
"-v",
|
|
13748
|
-
`${host}:/to`,
|
|
13749
|
-
"alpine:3",
|
|
13750
|
-
"sh",
|
|
13751
|
-
"-c",
|
|
13752
|
-
"cp -a /from/. /to/ 2>/dev/null || true"
|
|
13753
|
-
],
|
|
13754
|
-
{ stdio: "inherit" }
|
|
13755
|
-
);
|
|
13756
|
-
if (copy.status !== 0) {
|
|
13757
|
-
console.log(
|
|
13758
|
-
` Could not copy ${volume} \u2014 the stack will start with an empty ${host}.
|
|
13759
|
-
Recover manually with: docker run --rm -v ${volume}:/from -v ${host}:/to alpine cp -a /from/. /to/`
|
|
13760
|
-
);
|
|
13761
|
-
}
|
|
13762
|
-
}
|
|
13763
|
-
}
|
|
13764
|
-
function warnOrphanedVolumes(project) {
|
|
13765
|
-
const r2 = spawnSync3("docker", ["volume", "ls", "--format", "{{.Name}}"], { encoding: "utf8" });
|
|
13766
|
-
if (r2.status !== 0 || !r2.stdout) return;
|
|
13767
|
-
const others = new Set(
|
|
13768
|
-
r2.stdout.split("\n").map((l2) => l2.trim()).filter((l2) => l2.endsWith("_openship_sites")).map((l2) => l2.slice(0, -"_openship_sites".length)).filter((p2) => p2 && p2 !== project)
|
|
13769
|
-
);
|
|
13770
|
-
if (others.size === 0) return;
|
|
13771
|
-
console.log(
|
|
13772
|
-
` Note: volumes from a previous install (project "${[...others].join(", ")}") are still on disk
|
|
13773
|
-
and are NOT mounted by this stack \u2014 its database starts fresh. (Certificates and
|
|
13774
|
-
vhosts now live on the host, so those carry over.)
|
|
13775
|
-
Remove them with \`docker volume ls | grep _openship_\` once you're sure they aren't needed.`
|
|
13776
|
-
);
|
|
13777
|
-
}
|
|
13778
|
-
function keepConfig(prev, key, explicit) {
|
|
13779
|
-
const set = explicit?.trim();
|
|
13780
|
-
if (set) return set;
|
|
13781
|
-
const carried = prev[key]?.trim();
|
|
13782
|
-
return carried || void 0;
|
|
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
|
-
}
|
|
13801
|
-
function resolveEnvConfig(prev, opts) {
|
|
13802
|
-
const publicUrl = keepConfig(prev, "OPENSHIP_PUBLIC_URL", opts.publicUrl);
|
|
13803
|
-
const bindAddr = keepConfig(prev, "OPENSHIP_BIND_ADDR") ?? (publicUrl ? "0.0.0.0" : void 0);
|
|
13804
|
-
return {
|
|
13805
|
-
apiPort: keepConfig(prev, "API_PORT", opts.apiPort) ?? String(DEFAULT_API_PORT),
|
|
13806
|
-
dashPort: keepConfig(prev, "DASHBOARD_PORT", opts.dashboardPort) ?? String(DEFAULT_DASHBOARD_PORT),
|
|
13807
|
-
...bindAddr ? { bindAddr } : {},
|
|
13808
|
-
...publicUrl ? { publicUrl } : {},
|
|
13809
|
-
// A public URL always implies a proxy in front; otherwise keep whatever the
|
|
13810
|
-
// install was configured with.
|
|
13811
|
-
trustProxy: Boolean(opts.trustProxy) || Boolean(publicUrl) || prev.TRUST_PROXY === "true",
|
|
13812
|
-
...keepConfig(prev, "OPENSHIP_EXTRA_TRUSTED_ORIGINS", opts.extraTrustedOrigins) ? {
|
|
13813
|
-
extraTrustedOrigins: keepConfig(
|
|
13814
|
-
prev,
|
|
13815
|
-
"OPENSHIP_EXTRA_TRUSTED_ORIGINS",
|
|
13816
|
-
opts.extraTrustedOrigins
|
|
13817
|
-
)
|
|
13818
|
-
} : {},
|
|
13819
|
-
registry: keepConfig(prev, "OPENSHIP_IMAGE_REGISTRY", opts.registry) ?? DEFAULT_IMAGE_REGISTRY,
|
|
13820
|
-
// Tri-state: the flag is absent on a plain re-run, so fall back to what the
|
|
13821
|
-
// install chose rather than silently re-granting host control.
|
|
13822
|
-
hostControl: opts.noHostControl === void 0 ? prev.OPENSHIP_HOST_CONTROL !== "false" : !opts.noHostControl
|
|
13823
|
-
};
|
|
13824
|
-
}
|
|
13825
|
-
function toPort(value) {
|
|
13826
|
-
const n2 = Number(value);
|
|
13827
|
-
return Number.isInteger(n2) && n2 > 0 && n2 <= 65535 ? n2 : void 0;
|
|
13828
|
-
}
|
|
13829
|
-
function composeEnvPorts() {
|
|
13830
|
-
const env = readEnvFile();
|
|
13831
|
-
return { api: toPort(env.API_PORT), dashboard: toPort(env.DASHBOARD_PORT) };
|
|
13832
|
-
}
|
|
13833
|
-
function composeBindAddr() {
|
|
13834
|
-
return readEnvFile().OPENSHIP_BIND_ADDR?.trim() || "127.0.0.1";
|
|
13835
|
-
}
|
|
13836
|
-
function composeTrustedOriginUrls() {
|
|
13837
|
-
const env = readEnvFile();
|
|
13838
|
-
return [env.OPENSHIP_PUBLIC_URL, env.OPENSHIP_EXTRA_TRUSTED_ORIGINS].filter(
|
|
13839
|
-
(v2) => !!v2?.trim()
|
|
13840
|
-
);
|
|
13841
|
-
}
|
|
13842
|
-
function composeHeldPorts() {
|
|
13843
|
-
const r2 = spawnSync3(
|
|
13844
|
-
"docker",
|
|
13845
|
-
["ps", "--format", '{{.Label "com.docker.compose.project.config_files"}} {{.Ports}}'],
|
|
13846
|
-
{ encoding: "utf8" }
|
|
13847
|
-
);
|
|
13848
|
-
if (r2.status !== 0 || !r2.stdout) return [];
|
|
13849
|
-
const ports = /* @__PURE__ */ new Set();
|
|
13850
|
-
for (const line of r2.stdout.split("\n")) {
|
|
13851
|
-
const [configFiles = "", published = ""] = line.split(" ");
|
|
13852
|
-
if (!configFiles.split(",").some((f2) => f2.trim() === COMPOSE_FILE)) continue;
|
|
13853
|
-
for (const m2 of published.matchAll(/:(\d+)->/g)) ports.add(Number(m2[1]));
|
|
13854
|
-
}
|
|
13855
|
-
return [...ports];
|
|
13856
|
-
}
|
|
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
|
-
);
|
|
13868
|
-
}
|
|
13869
|
-
function renderEnv(opts, host, cfg) {
|
|
13870
|
-
const prev = readEnvFile();
|
|
13871
|
-
const lines = [
|
|
13872
|
-
"# Managed by `openship up`. Secrets are generated once and preserved.",
|
|
13873
|
-
// Do NOT edit on a live install — it is the volume prefix (see composeProjectName).
|
|
13874
|
-
`COMPOSE_PROJECT_NAME=${composeProjectName(prev)}`,
|
|
13875
|
-
"CLOUD_MODE=false",
|
|
13876
|
-
"OPENSHIP_TARGET=local",
|
|
13877
|
-
"OPENSHIP_REQUIRE_AUTH=true",
|
|
13878
|
-
// Read by createHostExecutor (throws when false) and by the servers list
|
|
13879
|
-
// (hides the local row). Written explicitly so the policy is visible in .env.
|
|
13880
|
-
`OPENSHIP_HOST_CONTROL=${cfg.hostControl ? "true" : "false"}`,
|
|
13881
|
-
`OPENSHIP_IMAGE_REGISTRY=${cfg.registry}`,
|
|
13882
|
-
`OPENSHIP_VERSION=${opts.version || (true ? "0.6.1" : "latest")}`,
|
|
13883
|
-
`POSTGRES_PASSWORD=${keepSecret(prev, "POSTGRES_PASSWORD")}`,
|
|
13884
|
-
// Pinned once (see resolvePgData): fresh install → subdir, existing volume → root.
|
|
13885
|
-
`OPENSHIP_PGDATA=${resolvePgData(prev)}`,
|
|
13886
|
-
`BETTER_AUTH_SECRET=${keepSecret(prev, "BETTER_AUTH_SECRET")}`,
|
|
13887
|
-
`INTERNAL_TOKEN=${keepSecret(prev, "INTERNAL_TOKEN")}`,
|
|
13888
|
-
`API_PORT=${cfg.apiPort}`,
|
|
13889
|
-
`DASHBOARD_PORT=${cfg.dashPort}`,
|
|
13890
|
-
// The api's OWN view of the dashboard port: the self-app boot reconcile points
|
|
13891
|
-
// the operator's domain at the dashboard through this (self-deploy.ts), and it
|
|
13892
|
-
// silently defaults to 3001 when unset. Ports are dynamic now, so leaving it
|
|
13893
|
-
// out publishes a domain routed to a port nothing is listening on.
|
|
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)
|
|
13898
|
-
];
|
|
13899
|
-
if (cfg.bindAddr) lines.push(`OPENSHIP_BIND_ADDR=${cfg.bindAddr}`);
|
|
13900
|
-
if (cfg.publicUrl) lines.push(`OPENSHIP_PUBLIC_URL=${cfg.publicUrl}`);
|
|
13901
|
-
if (cfg.extraTrustedOrigins) {
|
|
13902
|
-
lines.push(`OPENSHIP_EXTRA_TRUSTED_ORIGINS=${cfg.extraTrustedOrigins}`);
|
|
13903
|
-
}
|
|
13904
|
-
if (cfg.trustProxy) lines.push("TRUST_PROXY=true");
|
|
13905
|
-
if (host) {
|
|
13906
|
-
lines.push(
|
|
13907
|
-
"OPENSHIP_HOST_SSH_HOST=host.docker.internal",
|
|
13908
|
-
`OPENSHIP_HOST_SSH_USER=${host.user}`,
|
|
13909
|
-
"OPENSHIP_HOST_SSH_PORT=22",
|
|
13910
|
-
"OPENSHIP_HOST_SSH_KEY=/run/secrets/openship_host_key",
|
|
13911
|
-
`OPENSHIP_HOST_KEY_PATH=${host.keyPath}`
|
|
13912
|
-
);
|
|
13913
|
-
}
|
|
13914
|
-
return lines.join("\n") + "\n";
|
|
13915
|
-
}
|
|
13916
|
-
function sourceBuildDir() {
|
|
13917
|
-
const marker = readSourceInstall();
|
|
13918
|
-
if (!marker?.dir) return null;
|
|
13919
|
-
const hasAll = BUILT_SERVICES.every((s2) => existsSync4(join7(marker.dir, s2.dockerfile)));
|
|
13920
|
-
return hasAll ? marker.dir : null;
|
|
13921
|
-
}
|
|
13922
|
-
function renderBuildOverride(repoDir) {
|
|
13923
|
-
const services = BUILT_SERVICES.map(
|
|
13924
|
-
(s2) => ` ${s2.service}:
|
|
13925
|
-
build:
|
|
13926
|
-
context: ${repoDir}
|
|
13927
|
-
dockerfile: ${s2.dockerfile}
|
|
13928
|
-
`
|
|
13929
|
-
).join("");
|
|
13930
|
-
return `# Managed by \`openship up\` (from-source install) \u2014 builds instead of pulls.
|
|
13931
|
-
services:
|
|
13932
|
-
${services}`;
|
|
13933
|
-
}
|
|
13934
|
-
function materialize(opts) {
|
|
13935
|
-
mkdirSync3(COMPOSE_DIR, { recursive: true, mode: 448 });
|
|
13936
|
-
const prev = readEnvFile();
|
|
13937
|
-
const cfg = resolveEnvConfig(prev, opts);
|
|
13938
|
-
const host = provisionHostSshChannel(cfg.hostControl);
|
|
13939
|
-
let before = "";
|
|
13940
|
-
try {
|
|
13941
|
-
before = readFileSync3(ENV_FILE, "utf8");
|
|
13942
|
-
} catch {
|
|
13943
|
-
}
|
|
13944
|
-
const rendered = renderEnv(opts, host, cfg);
|
|
13945
|
-
writeFileSync2(COMPOSE_FILE, COMPOSE_YAML);
|
|
13946
|
-
writeFileSync2(ENV_FILE, rendered, { mode: 384 });
|
|
13947
|
-
const buildDir = opts.build === false ? null : sourceBuildDir();
|
|
13948
|
-
if (buildDir) writeFileSync2(BUILD_FILE, renderBuildOverride(buildDir));
|
|
13949
|
-
return { buildDir, cfg, envChanged: before !== "" && rendered !== before };
|
|
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
|
-
}
|
|
13985
|
-
function dbVolumeExists(project) {
|
|
13986
|
-
const r2 = spawnSync3("docker", ["volume", "inspect", `${project}_postgres_data`], {
|
|
13987
|
-
stdio: "ignore"
|
|
13988
|
-
});
|
|
13989
|
-
return r2.status === 0;
|
|
13990
|
-
}
|
|
13991
|
-
function compose(args, opts) {
|
|
13992
|
-
const files = ["-f", COMPOSE_FILE];
|
|
13993
|
-
if (opts?.withBuildOverride) files.push("-f", BUILD_FILE);
|
|
13994
|
-
const r2 = spawnSync3("docker", ["compose", ...files, ...args], {
|
|
13995
|
-
cwd: COMPOSE_DIR,
|
|
13996
|
-
stdio: opts?.quiet ? "ignore" : "inherit"
|
|
13997
|
-
});
|
|
13998
|
-
return r2.status ?? 1;
|
|
13999
|
-
}
|
|
14000
|
-
function composePrefetch(opts) {
|
|
14001
|
-
const { buildDir } = materialize(opts);
|
|
14002
|
-
if (buildDir) {
|
|
14003
|
-
return compose(["pull", "postgres", "redis"], { withBuildOverride: true }) === 0 && compose(["build"], { withBuildOverride: true }) === 0;
|
|
14004
|
-
}
|
|
14005
|
-
return compose(["pull"]) === 0;
|
|
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
|
-
}
|
|
14024
|
-
async function composeUp(opts) {
|
|
14025
|
-
const { buildDir, cfg, envChanged } = materialize(opts);
|
|
14026
|
-
const apiPort = cfg.apiPort;
|
|
14027
|
-
const dashPort = cfg.dashPort;
|
|
14028
|
-
const up = (extra = {}) => compose(
|
|
14029
|
-
envChanged ? ["up", "-d", "--force-recreate", ...ENV_CONSUMING_SERVICES] : ["up", "-d"],
|
|
14030
|
-
extra
|
|
14031
|
-
);
|
|
14032
|
-
const env = readEnvFile();
|
|
14033
|
-
const project = env.COMPOSE_PROJECT_NAME || "openship";
|
|
14034
|
-
removeOrphanedStack(project);
|
|
14035
|
-
reconcileEdgeContainerName(project);
|
|
14036
|
-
migrateLegacyEdgeVolumes(project);
|
|
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
|
-
}
|
|
14053
|
-
if (dbVolumeExists(project)) {
|
|
14054
|
-
reconcileDbPassword(env.POSTGRES_USER || "openship", env.POSTGRES_PASSWORD ?? "");
|
|
14055
|
-
}
|
|
14056
|
-
await sanitizeEdgeVhosts(
|
|
14057
|
-
new LocalExecutor(),
|
|
14058
|
-
EDGE_SITES_HOST_DIR,
|
|
14059
|
-
(l2) => console.log(` ${l2.message}`)
|
|
14060
|
-
).catch(() => {
|
|
14061
|
-
});
|
|
14062
|
-
if (buildDir) {
|
|
14063
|
-
if (!opts.alreadyFetched) {
|
|
14064
|
-
if (compose(["pull", "postgres", "redis"], { withBuildOverride: true }) !== 0) {
|
|
14065
|
-
return { ok: false, apiPort, dashPort };
|
|
14066
|
-
}
|
|
14067
|
-
if (compose(["build"], { withBuildOverride: true }) !== 0) {
|
|
14068
|
-
return { ok: false, apiPort, dashPort };
|
|
14069
|
-
}
|
|
14070
|
-
}
|
|
14071
|
-
if (up({ withBuildOverride: true }) !== 0) {
|
|
14072
|
-
return { ok: false, apiPort, dashPort };
|
|
14073
|
-
}
|
|
14074
|
-
onEdgeContainerChanged();
|
|
14075
|
-
writeInstallMethod("compose");
|
|
14076
|
-
return { ok: true, apiPort, dashPort };
|
|
14077
|
-
}
|
|
14078
|
-
if (!opts.alreadyFetched && compose(["pull"]) !== 0) {
|
|
14079
|
-
return { ok: false, apiPort, dashPort };
|
|
14080
|
-
}
|
|
14081
|
-
if (up() !== 0) return { ok: false, apiPort, dashPort };
|
|
14082
|
-
onEdgeContainerChanged();
|
|
14083
|
-
writeInstallMethod("compose");
|
|
14084
|
-
return { ok: true, apiPort, dashPort };
|
|
14085
|
-
}
|
|
14086
|
-
function onEdgeContainerChanged() {
|
|
14087
|
-
invalidateEdgeContainer();
|
|
14088
|
-
}
|
|
14089
|
-
function composeDown() {
|
|
14090
|
-
if (!existsSync4(COMPOSE_FILE)) return false;
|
|
14091
|
-
return compose(["down"]) === 0;
|
|
14092
|
-
}
|
|
14093
|
-
function composeUninstall(opts = {}) {
|
|
14094
|
-
const removedImages = [];
|
|
14095
|
-
const ok = existsSync4(COMPOSE_FILE) ? compose(["down", "-v", "--remove-orphans"]) === 0 : false;
|
|
14096
|
-
if (opts.removeImages) {
|
|
14097
|
-
const env = readEnvFile();
|
|
14098
|
-
const registry = env.OPENSHIP_IMAGE_REGISTRY || DEFAULT_IMAGE_REGISTRY;
|
|
14099
|
-
const version = env.OPENSHIP_VERSION || "latest";
|
|
14100
|
-
for (const { service } of BUILT_SERVICES) {
|
|
14101
|
-
const ref = `${registry}/openship-${service}:${version}`;
|
|
14102
|
-
if (spawnSync3("docker", ["image", "rm", "-f", ref], { stdio: "ignore" }).status === 0) {
|
|
14103
|
-
removedImages.push(ref);
|
|
14104
|
-
}
|
|
14105
|
-
}
|
|
14106
|
-
}
|
|
14107
|
-
return { ok, removedImages };
|
|
14108
|
-
}
|
|
14109
|
-
async function composeUpdate(version) {
|
|
14110
|
-
if (!existsSync4(COMPOSE_FILE)) return false;
|
|
14111
|
-
return (await composeUp(version ? { version } : {})).ok;
|
|
14112
|
-
}
|
|
14113
|
-
function composePs() {
|
|
14114
|
-
return compose(["ps"]);
|
|
14115
|
-
}
|
|
14116
|
-
function composeRunning() {
|
|
14117
|
-
const r2 = spawnSync3(
|
|
14118
|
-
"docker",
|
|
14119
|
-
["compose", "-f", COMPOSE_FILE, "ps", "--status", "running", "-q"],
|
|
14120
|
-
{ cwd: COMPOSE_DIR, encoding: "utf8" }
|
|
14121
|
-
);
|
|
14122
|
-
return r2.status === 0 && r2.stdout.trim().length > 0;
|
|
14123
|
-
}
|
|
14124
|
-
function composeStart() {
|
|
14125
|
-
reconcileEdgeContainerName(readEnvFile().COMPOSE_PROJECT_NAME || "openship");
|
|
14126
|
-
return compose(["up", "-d"], { quiet: true }) === 0;
|
|
14127
|
-
}
|
|
14128
|
-
function composeRestart() {
|
|
14129
|
-
return compose(["restart"], { quiet: true }) === 0;
|
|
14130
|
-
}
|
|
14131
|
-
function composeInternalToken() {
|
|
14132
|
-
return readEnvFile().INTERNAL_TOKEN ?? null;
|
|
14133
|
-
}
|
|
14134
|
-
|
|
14135
|
-
// src/lib/edge-import.ts
|
|
14136
|
-
async function waitForApiHealth(port, tries) {
|
|
14137
|
-
for (let i2 = 0; i2 < tries; i2++) {
|
|
14138
|
-
try {
|
|
14139
|
-
const r2 = await fetch(`http://127.0.0.1:${port}/api/health`, { signal: AbortSignal.timeout(2e3) });
|
|
14140
|
-
if (r2.ok) return true;
|
|
14141
|
-
} catch {
|
|
14142
|
-
}
|
|
14143
|
-
await new Promise((r2) => setTimeout(r2, 1e3));
|
|
14144
|
-
}
|
|
14145
|
-
return false;
|
|
14146
|
-
}
|
|
14147
|
-
async function waitForEdgeRunning(tries) {
|
|
14148
|
-
const exec = new LocalExecutor();
|
|
14149
|
-
for (let i2 = 0; i2 < tries; i2++) {
|
|
14150
|
-
const out = await exec.exec(`docker inspect -f '{{.State.Running}}' ${EDGE_CONTAINER_NAME}`).catch(() => "");
|
|
14151
|
-
if (out.trim() === "true") return true;
|
|
14152
|
-
await new Promise((r2) => setTimeout(r2, 1e3));
|
|
14153
|
-
}
|
|
14154
|
-
return false;
|
|
14155
|
-
}
|
|
14156
|
-
async function importMigratedSites(apiPort, sites, certPems, staticRootOverrides) {
|
|
14157
|
-
const token = composeInternalToken();
|
|
14158
|
-
if (!token) {
|
|
14159
|
-
const error = "couldn't read the stack's internal token";
|
|
14160
|
-
console.log(chalk.yellow(` Skipping site import \u2014 ${error}. Re-run to retry.
|
|
14161
|
-
`));
|
|
14162
|
-
return { ok: false, registered: [], error };
|
|
14163
|
-
}
|
|
14164
|
-
const spinner = ora(
|
|
14165
|
-
`Importing ${sites.length} migrated site${sites.length === 1 ? "" : "s"} into the edge\u2026`
|
|
14166
|
-
).start();
|
|
14167
|
-
if (!await waitForApiHealth(apiPort, 60)) {
|
|
14168
|
-
const error = "API didn't become healthy in time";
|
|
14169
|
-
spinner.warn(`${error} \u2014 migrated sites not imported. Re-run \`openship up\` to retry.`);
|
|
14170
|
-
return { ok: false, registered: [], error };
|
|
14171
|
-
}
|
|
14172
|
-
if (!await waitForEdgeRunning(60)) {
|
|
14173
|
-
const error = `the ${EDGE_CONTAINER_NAME} container isn't running yet`;
|
|
14174
|
-
spinner.warn(`${error} \u2014 migrated sites not imported. Re-run \`openship up\` to retry.`);
|
|
14175
|
-
return { ok: false, registered: [], error };
|
|
14176
|
-
}
|
|
14177
|
-
try {
|
|
14178
|
-
const r2 = await fetch(`http://127.0.0.1:${apiPort}/api/system/edge/import-sites`, {
|
|
14179
|
-
method: "POST",
|
|
14180
|
-
headers: { "Content-Type": "application/json", "X-Internal-Token": token },
|
|
14181
|
-
body: JSON.stringify({ sites, certPems, staticRootOverrides }),
|
|
14182
|
-
signal: AbortSignal.timeout(12e4)
|
|
14183
|
-
});
|
|
14184
|
-
const data = await r2.json().catch(() => ({}));
|
|
14185
|
-
if (!r2.ok) {
|
|
14186
|
-
const error = data.error ?? `HTTP ${r2.status}`;
|
|
14187
|
-
spinner.warn(`Site import failed: ${error}. Re-run to retry.`);
|
|
14188
|
-
return { ok: false, registered: [], error };
|
|
14189
|
-
}
|
|
14190
|
-
const registered = data.registered ?? [];
|
|
14191
|
-
const warnings = data.warnings ?? [];
|
|
14192
|
-
const missed = sites.length - registered.length;
|
|
14193
|
-
if (sites.length > 0 && registered.length === 0) {
|
|
14194
|
-
spinner.fail(
|
|
14195
|
-
`Migrated NONE of the ${sites.length} site${sites.length === 1 ? "" : "s"} \u2014 Openship holds :80/:443 and ${sites.length === 1 ? "that hostname is" : "those hostnames are"} NOT being served.`
|
|
14196
|
-
);
|
|
14197
|
-
for (const w2 of warnings.slice(0, 8)) console.log(chalk.red(` \u2022 ${w2}`));
|
|
14198
|
-
const edgeLog = await edgeCrashReason(new LocalExecutor());
|
|
14199
|
-
if (edgeLog) {
|
|
14200
|
-
console.log(chalk.red(`
|
|
14201
|
-
The edge container is not running. Its log says:`));
|
|
14202
|
-
console.log(chalk.red(` ${edgeLog}`));
|
|
14203
|
-
}
|
|
14204
|
-
console.log(
|
|
14205
|
-
chalk.yellow(
|
|
14206
|
-
`
|
|
14207
|
-
Retry the import with \`openship up\` once the cause above is fixed, or put your
|
|
14208
|
-
previous proxy back: docker stop ${EDGE_CONTAINER_NAME} && sudo systemctl enable --now nginx
|
|
14209
|
-
`
|
|
14210
|
-
)
|
|
14211
|
-
);
|
|
14212
|
-
return { ok: false, registered, error: warnings[0] ?? "no sites were registered" };
|
|
14213
|
-
}
|
|
14214
|
-
if (missed > 0) {
|
|
14215
|
-
spinner.warn(
|
|
14216
|
-
`Migrated ${registered.length}/${sites.length} sites \u2014 ${missed} not served (see below).`
|
|
14217
|
-
);
|
|
14218
|
-
for (const w2 of warnings.slice(0, 8)) console.log(chalk.yellow(` \u2022 ${w2}`));
|
|
14219
|
-
return { ok: false, registered, error: `${missed} site(s) not registered` };
|
|
14220
|
-
}
|
|
14221
|
-
spinner.succeed(
|
|
14222
|
-
`Migrated ${registered.length} site${registered.length === 1 ? "" : "s"} into Openship's edge.`
|
|
14223
|
-
);
|
|
14224
|
-
for (const w2 of warnings.slice(0, 8)) console.log(chalk.dim(` \u2022 ${w2}`));
|
|
14225
|
-
return { ok: true, registered };
|
|
14226
|
-
} catch (e2) {
|
|
14227
|
-
const error = e2.message;
|
|
14228
|
-
spinner.warn(`Site import failed: ${error}. Re-run to retry.`);
|
|
14229
|
-
return { ok: false, registered: [], error };
|
|
13112
|
+
const env = {};
|
|
13113
|
+
for (const line of text.split("\n")) {
|
|
13114
|
+
const m2 = line.match(/^([A-Z0-9_]+)=(.*)$/);
|
|
13115
|
+
if (m2) env[m2[1]] = m2[2];
|
|
14230
13116
|
}
|
|
13117
|
+
return { env, unreadable: null };
|
|
14231
13118
|
}
|
|
14232
13119
|
|
|
14233
13120
|
export {
|
|
14234
|
-
|
|
14235
|
-
|
|
14236
|
-
|
|
14237
|
-
|
|
14238
|
-
|
|
14239
|
-
|
|
14240
|
-
|
|
14241
|
-
|
|
14242
|
-
|
|
14243
|
-
readStoredPorts,
|
|
14244
|
-
storedApiPort,
|
|
14245
|
-
storedDashboardPort,
|
|
14246
|
-
portMoveNotice,
|
|
14247
|
-
resolvePorts,
|
|
14248
|
-
DEFAULT_REPO,
|
|
14249
|
-
SOURCE_CHECKOUT_DIR,
|
|
14250
|
-
SOURCE_DIST_DIR,
|
|
14251
|
-
has,
|
|
14252
|
-
shortSha,
|
|
14253
|
-
prepareFromSource,
|
|
14254
|
-
readSourceInstall,
|
|
14255
|
-
remoteSha,
|
|
14256
|
-
rebuildFromSource,
|
|
14257
|
-
readInstallMethod,
|
|
14258
|
-
dockerGap,
|
|
14259
|
-
hasDockerCompose,
|
|
14260
|
-
dockerInstallPreview,
|
|
14261
|
-
ensureDocker,
|
|
14262
|
-
composeTrustedOriginUrls,
|
|
14263
|
-
resolveComposePorts,
|
|
14264
|
-
sourceBuildDir,
|
|
14265
|
-
composePlan,
|
|
14266
|
-
composePrefetch,
|
|
14267
|
-
composeUp,
|
|
14268
|
-
composeDown,
|
|
14269
|
-
composeUninstall,
|
|
14270
|
-
composeUpdate,
|
|
14271
|
-
composePs,
|
|
14272
|
-
composeRunning,
|
|
14273
|
-
composeStart,
|
|
14274
|
-
composeRestart,
|
|
14275
|
-
composeInternalToken,
|
|
14276
|
-
waitForApiHealth,
|
|
14277
|
-
waitForEdgeRunning,
|
|
14278
|
-
importMigratedSites
|
|
13121
|
+
resolveLocalEnvironmentSync,
|
|
13122
|
+
thisHost,
|
|
13123
|
+
pasteable,
|
|
13124
|
+
startUnitHint,
|
|
13125
|
+
COMPOSE_DIR,
|
|
13126
|
+
COMPOSE_FILE,
|
|
13127
|
+
COMPOSE_ENV_FILE,
|
|
13128
|
+
composeInstallExists,
|
|
13129
|
+
readComposeEnvFile
|
|
14279
13130
|
};
|