openship 0.4.7 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/{bare-2HYDXQEH.js → bare-VRGE5MXH.js} +7 -7
- package/dist/{chunk-TGIINPVM.js → chunk-6LKHXB6T.js} +22 -7
- package/dist/{chunk-R5BHFFAV.js → chunk-6PIYRG5I.js} +56 -17
- package/dist/{chunk-IMBD5PZ5.js → chunk-BS2AXYZX.js} +430 -86
- package/dist/{chunk-SJIFLLX4.js → chunk-CUJXGK7R.js} +321 -85
- package/dist/{chunk-VFINNTUL.js → chunk-EAAGSQXN.js} +23 -8
- package/dist/{chunk-4Z5QO7N6.js → chunk-I6PSTQIR.js} +2 -2
- package/dist/{chunk-XFCDYZUQ.js → chunk-JLKKYUZA.js} +31 -49
- package/dist/{chunk-4TJC4JZQ.js → chunk-K2RPVGPN.js} +23 -4
- package/dist/{chunk-MZ2WEHHY.js → chunk-KNTJ7PSF.js} +1 -1
- package/dist/{chunk-L2VOGB2X.js → chunk-KZOBYJUW.js} +1 -1
- package/dist/{chunk-IPYPMI3S.js → chunk-NNPEG6HM.js} +1 -1
- package/dist/{chunk-UXWAVM4C.js → chunk-NXFIRK2Z.js} +398 -112
- package/dist/{chunk-FXTBHTWY.js → chunk-RB35YN3J.js} +271 -35
- package/dist/{chunk-6APULWSU.js → chunk-UMQP7SYT.js} +429 -66
- package/dist/{chunk-V6AN6XLZ.js → chunk-V7BZMGOT.js} +123 -45
- package/dist/{chunk-VDMQJHPI.js → chunk-YWMODKWL.js} +67 -4
- package/dist/{chunk-OXWDY22G.js → chunk-ZCOQJCZO.js} +1 -1
- package/dist/{cloud-2KXREXMO.js → cloud-3M2WZ624.js} +12 -12
- package/dist/{detect-2XSCFFTF.js → detect-BAJMY6KM.js} +7 -5
- package/dist/{docker-LODK5Y2S.js → docker-2NGBRJFJ.js} +9 -7
- package/dist/{docker-edge-executor-VHW3JPJS.js → docker-edge-executor-5I4M5QT7.js} +1 -1
- package/dist/{edge-import-ZQ4Y5HEA.js → edge-import-RIWNEIXQ.js} +21 -19
- package/dist/{ensure-container-edge-ED25XL3Z.js → ensure-container-edge-3NSYKHRX.js} +6 -6
- package/dist/{executor-YPT6RH6O.js → executor-LOYMPU67.js} +3 -3
- package/dist/index.js +309 -338
- package/dist/{nginx-XWJAOGS4.js → nginx-QE4DOM5J.js} +4 -4
- package/dist/{openresty-lua-4WLGDSER.js → openresty-lua-BTAKECCF.js} +2 -2
- package/dist/server/index.js +51705 -45369
- package/dist/server/migrations/0073_project_volumes_object_storage.sql +3 -0
- package/dist/server/migrations/0074_project_compose_path.sql +1 -0
- package/dist/server/migrations/0075_service_command_argv.sql +1 -0
- package/dist/server/migrations/0076_rollback_retention.sql +17 -0
- package/dist/server/migrations/0077_domain_redirect.sql +13 -0
- package/dist/server/migrations/0078_grant_source_scope.sql +30 -0
- package/dist/server/migrations/0079_project_readiness.sql +18 -0
- package/dist/server/migrations/0080_deployment_error_classification.sql +26 -0
- package/dist/server/migrations/0081_project_readiness_repair.sql +34 -0
- package/dist/server/migrations/meta/_journal.json +63 -0
- package/dist/{setup-2DMFIA74.js → setup-KELALK7H.js} +7 -7
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
resolveDockerfileCandidates,
|
|
10
10
|
resolveServiceDockerfile,
|
|
11
11
|
staticBuilderOutputPath
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-V7BZMGOT.js";
|
|
13
13
|
import {
|
|
14
14
|
BuildLogger,
|
|
15
15
|
assembleGitClone,
|
|
@@ -18,20 +18,24 @@ import {
|
|
|
18
18
|
shellGitSshWriter,
|
|
19
19
|
sq,
|
|
20
20
|
transferLocalDirectory
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-I6PSTQIR.js";
|
|
22
22
|
import {
|
|
23
23
|
connectSshClient,
|
|
24
24
|
execSshCommand,
|
|
25
25
|
openSshExecChannel,
|
|
26
26
|
openSshUnixSocket
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-ZCOQJCZO.js";
|
|
28
28
|
import {
|
|
29
29
|
isRuntimeNotFoundError
|
|
30
30
|
} from "./chunk-RPHRPFEH.js";
|
|
31
31
|
import {
|
|
32
|
+
formatCpuCores,
|
|
33
|
+
formatMemoryMb,
|
|
34
|
+
hasCpuLimit,
|
|
35
|
+
hasMemoryLimit,
|
|
32
36
|
safeErrorMessage,
|
|
33
37
|
withTimeout
|
|
34
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-UMQP7SYT.js";
|
|
35
39
|
|
|
36
40
|
// ../../packages/adapters/src/runtime/docker.ts
|
|
37
41
|
import Dockerode from "dockerode";
|
|
@@ -201,6 +205,59 @@ function daemonConnectionFrom(docker) {
|
|
|
201
205
|
};
|
|
202
206
|
}
|
|
203
207
|
|
|
208
|
+
// ../../packages/adapters/src/runtime/compose-cmd.ts
|
|
209
|
+
function resolveComposeCmd(config) {
|
|
210
|
+
if (config.commandArgv != null) return config.commandArgv;
|
|
211
|
+
return config.command ? ["sh", "-c", config.command] : void 0;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ../../packages/adapters/src/runtime/resource-limits.ts
|
|
215
|
+
function dockerResourceLimits(resources) {
|
|
216
|
+
const limits = {};
|
|
217
|
+
if (hasMemoryLimit(resources)) {
|
|
218
|
+
limits.Memory = Math.round(resources.memoryMb * 1024 * 1024);
|
|
219
|
+
}
|
|
220
|
+
if (hasCpuLimit(resources)) {
|
|
221
|
+
limits.NanoCpus = Math.max(1e6, Math.round(resources.cpuCores * 1e9));
|
|
222
|
+
}
|
|
223
|
+
return limits;
|
|
224
|
+
}
|
|
225
|
+
var CPU_PERIOD_US = 1e5;
|
|
226
|
+
function dockerBuildResourceLimits(resources) {
|
|
227
|
+
const limits = {};
|
|
228
|
+
if (hasMemoryLimit(resources)) {
|
|
229
|
+
limits.memory = Math.round(resources.memoryMb * 1024 * 1024);
|
|
230
|
+
}
|
|
231
|
+
if (hasCpuLimit(resources)) {
|
|
232
|
+
limits.cpuperiod = CPU_PERIOD_US;
|
|
233
|
+
limits.cpuquota = Math.max(1e3, Math.round(resources.cpuCores * CPU_PERIOD_US));
|
|
234
|
+
}
|
|
235
|
+
return limits;
|
|
236
|
+
}
|
|
237
|
+
function inspectResourceLimits(hostConfig) {
|
|
238
|
+
if (!hostConfig) return void 0;
|
|
239
|
+
const memoryMb = typeof hostConfig.Memory === "number" && hostConfig.Memory > 0 ? Math.round(hostConfig.Memory / (1024 * 1024)) : void 0;
|
|
240
|
+
let cpuCores;
|
|
241
|
+
if (typeof hostConfig.NanoCpus === "number" && hostConfig.NanoCpus > 0) {
|
|
242
|
+
cpuCores = hostConfig.NanoCpus / 1e9;
|
|
243
|
+
} else if (typeof hostConfig.CpuQuota === "number" && hostConfig.CpuQuota > 0 && typeof hostConfig.CpuPeriod === "number" && hostConfig.CpuPeriod > 0) {
|
|
244
|
+
cpuCores = hostConfig.CpuQuota / hostConfig.CpuPeriod;
|
|
245
|
+
}
|
|
246
|
+
if (cpuCores !== void 0) cpuCores = Math.round(cpuCores * 100) / 100;
|
|
247
|
+
if (memoryMb === void 0 && cpuCores === void 0) return void 0;
|
|
248
|
+
return {
|
|
249
|
+
...cpuCores !== void 0 && { cpuCores },
|
|
250
|
+
...memoryMb !== void 0 && { memoryMb }
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function describeResourceLimits(resources) {
|
|
254
|
+
const parts = [
|
|
255
|
+
hasCpuLimit(resources) ? formatCpuCores(resources.cpuCores) : null,
|
|
256
|
+
hasMemoryLimit(resources) ? formatMemoryMb(resources.memoryMb) : null
|
|
257
|
+
].filter(Boolean);
|
|
258
|
+
return parts.length ? parts.join(" \xB7 ") : "no limits (machine capacity)";
|
|
259
|
+
}
|
|
260
|
+
|
|
204
261
|
// ../../packages/adapters/src/runtime/docker-ssh-agent.ts
|
|
205
262
|
import net2 from "net";
|
|
206
263
|
var DEFAULT_REMOTE_DOCKER_SOCKET_PATH = "/var/run/docker.sock";
|
|
@@ -551,13 +608,29 @@ function createDockerSshBridge(opts) {
|
|
|
551
608
|
}
|
|
552
609
|
|
|
553
610
|
// ../../packages/adapters/src/runtime/docker-transport.ts
|
|
611
|
+
var DEFAULT_DOCKER_SOCKET_PATH = "/var/run/docker.sock";
|
|
612
|
+
function resolveLocalDockerSocketPath(opts, env = process.env) {
|
|
613
|
+
const explicit = opts?.dockerSocketPath?.trim();
|
|
614
|
+
if (explicit) return explicit;
|
|
615
|
+
const host = env.DOCKER_HOST?.trim();
|
|
616
|
+
if (host) {
|
|
617
|
+
if (host.startsWith("unix://")) {
|
|
618
|
+
const path = host.slice("unix://".length).trim();
|
|
619
|
+
if (path.startsWith("/")) return path;
|
|
620
|
+
} else if (host.startsWith("/")) {
|
|
621
|
+
return host;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return DEFAULT_DOCKER_SOCKET_PATH;
|
|
625
|
+
}
|
|
554
626
|
function resolveDockerTransport(opts) {
|
|
555
627
|
if (!opts || !opts.transport || opts.transport === "socket") {
|
|
628
|
+
const socketPath = resolveLocalDockerSocketPath(opts);
|
|
556
629
|
return {
|
|
557
630
|
kind: "socket",
|
|
558
|
-
description:
|
|
559
|
-
unreachableHint: "Check that the local Docker daemon is running."
|
|
560
|
-
establish: async () => ({ socketPath
|
|
631
|
+
description: `local Docker daemon via socket (${socketPath})`,
|
|
632
|
+
unreachableHint: socketPath === DEFAULT_DOCKER_SOCKET_PATH ? "Check that the local Docker daemon is running. If it isn't Docker Desktop (Colima, Rancher, Podman, rootless), set DOCKER_HOST to its socket." : `Check that the local Docker daemon is running and listening on ${socketPath} (from DOCKER_HOST / dockerSocketPath).`,
|
|
633
|
+
establish: async () => ({ socketPath }),
|
|
561
634
|
close: async () => {
|
|
562
635
|
},
|
|
563
636
|
preflight: async () => {
|
|
@@ -622,6 +695,9 @@ function resolveDockerTransport(opts) {
|
|
|
622
695
|
|
|
623
696
|
// ../../packages/adapters/src/runtime/docker.ts
|
|
624
697
|
var isDockerNotFoundError = isRuntimeNotFoundError;
|
|
698
|
+
function ownsBuiltImage(imageRef) {
|
|
699
|
+
return imageRef.startsWith("openship/");
|
|
700
|
+
}
|
|
625
701
|
function clampShellWindow(value, fallback, min, max) {
|
|
626
702
|
const n = Number.isFinite(value) ? Number(value) : fallback;
|
|
627
703
|
if (n < min) return min;
|
|
@@ -848,7 +924,8 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
848
924
|
"serviceShell",
|
|
849
925
|
"projectContainerSweep",
|
|
850
926
|
"deploymentContainerQuery",
|
|
851
|
-
"hostContainerQuery"
|
|
927
|
+
"hostContainerQuery",
|
|
928
|
+
"stabilityProbe"
|
|
852
929
|
]);
|
|
853
930
|
/** Docker honors every extended compose key we currently support. */
|
|
854
931
|
unsupportedComposeKeys = /* @__PURE__ */ new Set();
|
|
@@ -1265,6 +1342,10 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
1265
1342
|
...config.envVars,
|
|
1266
1343
|
NODE_ENV: "production"
|
|
1267
1344
|
},
|
|
1345
|
+
// Omitted entirely unless the project set a build cap — a self-hosted
|
|
1346
|
+
// build should be free to use the machine (a production build often
|
|
1347
|
+
// needs several GB). Opt-in only; see dockerBuildResourceLimits.
|
|
1348
|
+
...dockerBuildResourceLimits(config.resources),
|
|
1268
1349
|
forcerm: true
|
|
1269
1350
|
}
|
|
1270
1351
|
);
|
|
@@ -1483,12 +1564,16 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
1483
1564
|
async moveStaticBuildToHost(tag, config, hostOutDir, log) {
|
|
1484
1565
|
const docRoot = staticBuilderOutputPath(config);
|
|
1485
1566
|
const sshExecutor = this.transport.kind === "ssh" ? this.connectionOptions?.executor : null;
|
|
1486
|
-
log.log(
|
|
1567
|
+
log.log(`Moving built files out of the build container (${docRoot})...
|
|
1568
|
+
`);
|
|
1487
1569
|
try {
|
|
1488
1570
|
if (sshExecutor) {
|
|
1489
1571
|
await this.extractDocRootOverSsh(sshExecutor, tag, docRoot, hostOutDir);
|
|
1490
1572
|
} else {
|
|
1491
|
-
await this.
|
|
1573
|
+
const copiedInPlace = this.transport.kind === "socket" && await this.extractDocRootViaBindMount(tag, docRoot, hostOutDir);
|
|
1574
|
+
if (!copiedInPlace) {
|
|
1575
|
+
await this.extractDocRootViaDaemon(tag, docRoot, hostOutDir);
|
|
1576
|
+
}
|
|
1492
1577
|
}
|
|
1493
1578
|
log.log(`Static files ready at ${hostOutDir}
|
|
1494
1579
|
`);
|
|
@@ -1510,8 +1595,9 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
1510
1595
|
* a second web server was never going to run.
|
|
1511
1596
|
* Returns the host dir as `imageRef`, matching BareRuntime.build's host-dir
|
|
1512
1597
|
* contract so the existing file-backed serve path (deployStatic /
|
|
1513
|
-
* resolveStaticRoot) consumes it unchanged. Never leaves a long-lived
|
|
1514
|
-
*
|
|
1598
|
+
* resolveStaticRoot) consumes it unchanged. Never leaves a long-lived container:
|
|
1599
|
+
* the extraction container runs one `cp` (or isn't started at all, when the tar is
|
|
1600
|
+
* streamed instead) and is removed either way.
|
|
1515
1601
|
*/
|
|
1516
1602
|
async buildStaticToHost(config, hostOutDir, logger) {
|
|
1517
1603
|
const log = logger ?? new BuildLogger();
|
|
@@ -1544,18 +1630,31 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
1544
1630
|
}
|
|
1545
1631
|
}
|
|
1546
1632
|
/**
|
|
1547
|
-
* THE CONTRACT
|
|
1548
|
-
*
|
|
1549
|
-
*
|
|
1550
|
-
*
|
|
1633
|
+
* THE CONTRACT all three extractors satisfy: once one returns, `hostOutDir` holds
|
|
1634
|
+
* the doc-root's CONTENTS directly — `index.html` at the top, no wrapping
|
|
1635
|
+
* directory. OpenResty's `root` points straight at it, so one stray level is a
|
|
1636
|
+
* silent 404 for the whole site.
|
|
1637
|
+
*
|
|
1638
|
+
* They reach that shape differently, which is why the rule is stated here once
|
|
1639
|
+
* instead of in each branch:
|
|
1640
|
+
* - `docker cp <cid>:<root>/.` and `cp -a <root>/. <out>/` — the trailing `/.`
|
|
1641
|
+
* means "contents of", so there is nothing to strip.
|
|
1642
|
+
* - `getArchive({path: <root>})` — the tar is rooted at the doc-root's own
|
|
1643
|
+
* basename (`dist/`, `build/`, …), so exactly one leading component is
|
|
1644
|
+
* stripped.
|
|
1551
1645
|
*
|
|
1552
|
-
* The
|
|
1553
|
-
*
|
|
1554
|
-
*
|
|
1555
|
-
*
|
|
1556
|
-
*
|
|
1557
|
-
* one leading component must be stripped.
|
|
1646
|
+
* The other half of the contract is that hostOutDir is non-EMPTY, which is what
|
|
1647
|
+
* this enforces. It has to be checked per-path (three different filesystems) but
|
|
1648
|
+
* the rule and its wording live here, because the failure it prevents is the
|
|
1649
|
+
* expensive one: every extractor can succeed having copied nothing, which deploys
|
|
1650
|
+
* green and then 404s the entire site.
|
|
1558
1651
|
*/
|
|
1652
|
+
assertDocRootFilled(isEmpty, docRoot) {
|
|
1653
|
+
if (!isEmpty) return;
|
|
1654
|
+
throw new Error(
|
|
1655
|
+
`static extract produced no files from ${docRoot} in the build container \u2014 check the project's output directory setting.`
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1559
1658
|
/** Remote host: disk-to-disk via the host's own docker CLI — never stream a
|
|
1560
1659
|
* large tar over the SSH dockerode bridge (same rationale as saveImage/pullImage). */
|
|
1561
1660
|
async extractDocRootOverSsh(sshExecutor, tag, docRoot, hostOutDir) {
|
|
@@ -1563,34 +1662,126 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
1563
1662
|
try {
|
|
1564
1663
|
await sshExecutor.exec(`mkdir -p ${sq(hostOutDir)}`);
|
|
1565
1664
|
await sshExecutor.exec(`docker cp ${sq(`${cid}:${docRoot}/.`)} ${sq(hostOutDir)}`);
|
|
1665
|
+
const listing = (await sshExecutor.exec(`ls -A ${sq(hostOutDir)} | head -1`).catch(() => "")).trim();
|
|
1666
|
+
this.assertDocRootFilled(!listing, docRoot);
|
|
1566
1667
|
} finally {
|
|
1567
1668
|
await sshExecutor.exec(`docker rm ${sq(cid)}`).catch(() => {
|
|
1568
1669
|
});
|
|
1569
1670
|
}
|
|
1570
1671
|
}
|
|
1571
|
-
/**
|
|
1572
|
-
*
|
|
1573
|
-
*
|
|
1574
|
-
*
|
|
1672
|
+
/**
|
|
1673
|
+
* LOCAL DAEMON: let the daemon do the copy on its own disk. The builder image is
|
|
1674
|
+
* run once with `hostOutDir` bind-mounted, and it `cp -a`s the doc-root across.
|
|
1675
|
+
* No archive crosses this process at all.
|
|
1676
|
+
*
|
|
1677
|
+
* This exists because streaming the doc-root out via `getArchive` is undebuggable
|
|
1678
|
+
* when it goes wrong. Once the daemon has sent 200 + headers for an archive it has
|
|
1679
|
+
* NO way to report a mid-archive failure — an unreadable file, a socket/fifo, a
|
|
1680
|
+
* path it can't stat — so it just closes the connection. The client sees a
|
|
1681
|
+
* truncated body and the only symptom anywhere is `tar: Unexpected EOF in
|
|
1682
|
+
* archive`, with the actual cause visible solely in the daemon's own log. That is
|
|
1683
|
+
* the failure this method removes: `cp` runs in the container, so a real error
|
|
1684
|
+
* arrives as real stderr and a real exit code.
|
|
1685
|
+
*
|
|
1686
|
+
* Requires that the daemon's filesystem IS this process's filesystem, which is
|
|
1687
|
+
* true for DooD (`/opt/openship/static` is a host bind mount at the same path
|
|
1688
|
+
* inside and out — see docker/docker-compose.yml) and false for a VM-backed
|
|
1689
|
+
* daemon (Docker Desktop, Colima), where `hostOutDir` resolves inside the VM and
|
|
1690
|
+
* the copy would land somewhere this process can't see.
|
|
1691
|
+
*
|
|
1692
|
+
* So it PROVES the assumption instead of trusting the transport: a sentinel file
|
|
1693
|
+
* is written here and the container refuses to copy unless it can see it. A
|
|
1694
|
+
* daemon that can't reach our disk exits 97 and the caller streams instead. That
|
|
1695
|
+
* check is folded into the same container run — no extra round trip — because
|
|
1696
|
+
* getting it wrong means an empty doc-root, i.e. a site that deploys green and
|
|
1697
|
+
* serves 404s.
|
|
1698
|
+
*
|
|
1699
|
+
* Returns false when the bind isn't shared; throws on a real copy failure.
|
|
1700
|
+
*/
|
|
1701
|
+
async extractDocRootViaBindMount(tag, docRoot, hostOutDir) {
|
|
1702
|
+
const { mkdir, readdir, writeFile, rm } = await import("fs/promises");
|
|
1703
|
+
const { join } = await import("path");
|
|
1704
|
+
const { randomBytes } = await import("crypto");
|
|
1705
|
+
await mkdir(hostOutDir, { recursive: true });
|
|
1706
|
+
const OUT = "/__openship_out";
|
|
1707
|
+
const probe = `.openship-extract-probe-${randomBytes(6).toString("hex")}`;
|
|
1708
|
+
await writeFile(join(hostOutDir, probe), "probe");
|
|
1709
|
+
const container = await this.docker.createContainer({
|
|
1710
|
+
Image: tag,
|
|
1711
|
+
// Override the ENTRYPOINT: builder base images (node, bun) ship a
|
|
1712
|
+
// docker-entrypoint.sh that would swallow this Cmd.
|
|
1713
|
+
Entrypoint: ["/bin/sh", "-c"],
|
|
1714
|
+
Cmd: [
|
|
1715
|
+
`set -e; test -f ${sq(`${OUT}/${probe}`)} || exit 97; cp -a ${sq(docRoot)}/. ${OUT}/; rm -f ${sq(`${OUT}/${probe}`)}`
|
|
1716
|
+
],
|
|
1717
|
+
// Root, explicitly. A builder image that sets `USER node`/`USER bun` would
|
|
1718
|
+
// otherwise run this copy unprivileged and fail writing into a root-owned
|
|
1719
|
+
// output dir — the two paths this replaces both run with daemon privileges,
|
|
1720
|
+
// so anything less would be a NEW failure this change introduced.
|
|
1721
|
+
User: "0:0",
|
|
1722
|
+
HostConfig: {
|
|
1723
|
+
// `:z` matches how compose mounts this dir — required on SELinux hosts for
|
|
1724
|
+
// the container to write, and ignored where SELinux is off.
|
|
1725
|
+
Binds: [`${hostOutDir}:${OUT}:z`]
|
|
1726
|
+
}
|
|
1727
|
+
});
|
|
1728
|
+
try {
|
|
1729
|
+
try {
|
|
1730
|
+
await container.start();
|
|
1731
|
+
} catch {
|
|
1732
|
+
return false;
|
|
1733
|
+
}
|
|
1734
|
+
const status = await container.wait();
|
|
1735
|
+
if (status.StatusCode === 97) return false;
|
|
1736
|
+
if (status.StatusCode !== 0) {
|
|
1737
|
+
const logs = await container.logs({ stdout: true, stderr: true, tail: 40 }).then((b) => Buffer.from(b).toString("utf8")).then((s) => s.replace(/[\x00-\x08\x0b-\x1f\x7f]/g, " ").trim()).catch(() => "");
|
|
1738
|
+
throw new Error(
|
|
1739
|
+
`static extract failed copying ${docRoot} out of the build container (exit ${status.StatusCode})${logs ? `: ${logs.slice(-500)}` : ""}`
|
|
1740
|
+
);
|
|
1741
|
+
}
|
|
1742
|
+
const entries = (await readdir(hostOutDir).catch(() => [])).filter((e) => e !== probe);
|
|
1743
|
+
this.assertDocRootFilled(entries.length === 0, docRoot);
|
|
1744
|
+
return true;
|
|
1745
|
+
} finally {
|
|
1746
|
+
await container.remove({ force: true }).catch(() => {
|
|
1747
|
+
});
|
|
1748
|
+
await rm(join(hostOutDir, probe), { force: true }).catch(() => {
|
|
1749
|
+
});
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
/** Remote TCP daemon: no shared filesystem and no local `docker` CLI, so the tar
|
|
1753
|
+
* has to come through dockerode and be extracted onto THIS process's disk. */
|
|
1575
1754
|
async extractDocRootViaDaemon(tag, docRoot, hostOutDir) {
|
|
1576
|
-
const { mkdir } = await import("fs/promises");
|
|
1755
|
+
const { mkdir, readdir } = await import("fs/promises");
|
|
1577
1756
|
const { spawn } = await import("child_process");
|
|
1757
|
+
const { pipeline } = await import("stream/promises");
|
|
1578
1758
|
await mkdir(hostOutDir, { recursive: true });
|
|
1579
1759
|
const container = await this.docker.createContainer({ Image: tag });
|
|
1580
1760
|
try {
|
|
1581
1761
|
const tarStream = await container.getArchive({ path: docRoot });
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
extract.on("
|
|
1587
|
-
|
|
1588
|
-
tarStream.pipe(extract.stdin);
|
|
1589
|
-
extract.on(
|
|
1590
|
-
"close",
|
|
1591
|
-
(code) => code === 0 ? resolve() : reject(new Error(`static extract failed (tar ${code}): ${errBuf.trim().slice(-500)}`))
|
|
1592
|
-
);
|
|
1762
|
+
const extract = spawn("tar", ["-x", "--strip-components=1", "-C", hostOutDir]);
|
|
1763
|
+
let errBuf = "";
|
|
1764
|
+
extract.stderr.on("data", (d) => errBuf += d.toString());
|
|
1765
|
+
const exited = new Promise((resolve) => {
|
|
1766
|
+
extract.on("close", (code2) => resolve(code2 ?? 0));
|
|
1767
|
+
extract.on("error", () => resolve(-1));
|
|
1593
1768
|
});
|
|
1769
|
+
let streamError;
|
|
1770
|
+
await pipeline(tarStream, extract.stdin).catch((err) => {
|
|
1771
|
+
streamError = err;
|
|
1772
|
+
});
|
|
1773
|
+
const code = await exited;
|
|
1774
|
+
const tarErr = errBuf.trim();
|
|
1775
|
+
if (streamError && !(code !== 0 && tarErr)) {
|
|
1776
|
+
throw new Error(
|
|
1777
|
+
`static extract failed reading ${docRoot} from the build container: ${safeErrorMessage(streamError)}${tarErr ? ` (tar: ${tarErr.slice(-200)})` : ""}`
|
|
1778
|
+
);
|
|
1779
|
+
}
|
|
1780
|
+
if (code !== 0) {
|
|
1781
|
+
throw new Error(`static extract failed (tar ${code}): ${tarErr.slice(-500)}`);
|
|
1782
|
+
}
|
|
1783
|
+
const entries = await readdir(hostOutDir).catch(() => []);
|
|
1784
|
+
this.assertDocRootFilled(entries.length === 0, docRoot);
|
|
1594
1785
|
} finally {
|
|
1595
1786
|
await container.remove({ force: true }).catch(() => {
|
|
1596
1787
|
});
|
|
@@ -1717,6 +1908,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
1717
1908
|
sessionId: spec.config.sessionId
|
|
1718
1909
|
}),
|
|
1719
1910
|
buildargs: { ...spec.config.envVars, NODE_ENV: "production" },
|
|
1911
|
+
...dockerBuildResourceLimits(spec.config.resources),
|
|
1720
1912
|
forcerm: true
|
|
1721
1913
|
}
|
|
1722
1914
|
);
|
|
@@ -1805,9 +1997,29 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
1805
1997
|
];
|
|
1806
1998
|
const cmd = config.startCommand ? ["sh", "-c", config.startCommand] : void 0;
|
|
1807
1999
|
const restartPolicy = resolveRestartPolicy(config.restartPolicy);
|
|
2000
|
+
const scopedBinds = scopeVolumeBinds(
|
|
2001
|
+
config.slug || config.runtimeName || config.projectId,
|
|
2002
|
+
config.volumes ?? [],
|
|
2003
|
+
true
|
|
2004
|
+
);
|
|
2005
|
+
const binds = scopedBinds.length > 0 ? scopedBinds : void 0;
|
|
1808
2006
|
log({
|
|
1809
2007
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1810
2008
|
message: `Creating container ${containerName} from ${imageRef}...
|
|
2009
|
+
`,
|
|
2010
|
+
level: "info"
|
|
2011
|
+
});
|
|
2012
|
+
if (binds) {
|
|
2013
|
+
log({
|
|
2014
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2015
|
+
message: `Persistent storage: ${binds.join(", ")}
|
|
2016
|
+
`,
|
|
2017
|
+
level: "info"
|
|
2018
|
+
});
|
|
2019
|
+
}
|
|
2020
|
+
log({
|
|
2021
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2022
|
+
message: `Resource limits: ${describeResourceLimits(config.resources)}
|
|
1811
2023
|
`,
|
|
1812
2024
|
level: "info"
|
|
1813
2025
|
});
|
|
@@ -1823,8 +2035,10 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
1823
2035
|
ExposedPorts: { [`${config.port}/tcp`]: {} },
|
|
1824
2036
|
HostConfig: {
|
|
1825
2037
|
RestartPolicy: restartPolicy,
|
|
1826
|
-
|
|
1827
|
-
|
|
2038
|
+
Binds: binds,
|
|
2039
|
+
// Omitted entirely when the project has no cap (self-hosted default) —
|
|
2040
|
+
// see dockerResourceLimits. Never substitute a tier here.
|
|
2041
|
+
...dockerResourceLimits(config.resources),
|
|
1828
2042
|
// Publish on the LOOPBACK interface only — the edge (host process, or a
|
|
1829
2043
|
// host-net OpenResty container) reaches it at 127.0.0.1:<hostPort>, and
|
|
1830
2044
|
// it never faces the network. Binding 0.0.0.0 here would expose every
|
|
@@ -1969,44 +2183,24 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
1969
2183
|
serviceName: c.Labels?.["openship.service"]
|
|
1970
2184
|
}));
|
|
1971
2185
|
}
|
|
1972
|
-
// ── Rollback primitives
|
|
2186
|
+
// ── Rollback primitives (retention half only) ────────────────────────
|
|
1973
2187
|
//
|
|
1974
|
-
// Docker
|
|
1975
|
-
//
|
|
1976
|
-
//
|
|
1977
|
-
//
|
|
1978
|
-
//
|
|
1979
|
-
//
|
|
1980
|
-
//
|
|
1981
|
-
//
|
|
1982
|
-
//
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
if (input.to.containerId) {
|
|
1991
|
-
try {
|
|
1992
|
-
await this.start(input.to.containerId);
|
|
1993
|
-
return { containerId: input.to.containerId };
|
|
1994
|
-
} catch {
|
|
1995
|
-
}
|
|
1996
|
-
}
|
|
1997
|
-
if (!input.to.imageRef) {
|
|
1998
|
-
throw new Error(
|
|
1999
|
-
`Cannot make deployment ${input.to.id} active: container is gone and no imageRef is stored. Artifact has been purged.`
|
|
2000
|
-
);
|
|
2001
|
-
}
|
|
2002
|
-
const container = await this.docker.createContainer({
|
|
2003
|
-
Image: input.to.imageRef,
|
|
2004
|
-
name: `dep-${input.to.id}`,
|
|
2005
|
-
HostConfig: { RestartPolicy: { Name: "unless-stopped" } }
|
|
2006
|
-
});
|
|
2007
|
-
await container.start();
|
|
2008
|
-
return { containerId: container.id };
|
|
2009
|
-
}
|
|
2188
|
+
// Docker deliberately does NOT implement `makeActive` — see the
|
|
2189
|
+
// "unitRestore" capability. A redeploy REMOVES the previous container
|
|
2190
|
+
// (loopback-port routing can't overlap two containers on one host
|
|
2191
|
+
// port), so there is no unit left to restart; the durable artifact is
|
|
2192
|
+
// the IMAGE. Restore therefore re-materializes the container through
|
|
2193
|
+
// the normal deploy step from the target's frozen snapshot + retained
|
|
2194
|
+
// image (modules/deployments/rollback/restore-plan.ts), which is the
|
|
2195
|
+
// only way env, published port, volumes, labels, network and routing
|
|
2196
|
+
// all come back correctly.
|
|
2197
|
+
//
|
|
2198
|
+
// archive — `docker stop` (usually a no-op: the container is gone).
|
|
2199
|
+
// The image stays tagged, retained by the rollback-window
|
|
2200
|
+
// keep-set in modules/deployments/image-gc.
|
|
2201
|
+
// purge — `docker rm` (force) + `docker rmi`. Past this point an
|
|
2202
|
+
// instant restore of this deployment is impossible and
|
|
2203
|
+
// rollback degrades to a rebuild from its commit.
|
|
2010
2204
|
async archive(deployment) {
|
|
2011
2205
|
if (!deployment.containerId) return;
|
|
2012
2206
|
try {
|
|
@@ -2021,7 +2215,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
2021
2215
|
} catch {
|
|
2022
2216
|
}
|
|
2023
2217
|
}
|
|
2024
|
-
if (deployment.imageRef) {
|
|
2218
|
+
if (deployment.imageRef && ownsBuiltImage(deployment.imageRef)) {
|
|
2025
2219
|
try {
|
|
2026
2220
|
await this.removeImage(deployment.imageRef);
|
|
2027
2221
|
} catch {
|
|
@@ -2121,6 +2315,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
2121
2315
|
retries: hc.Retries,
|
|
2122
2316
|
startPeriod: hc.StartPeriod
|
|
2123
2317
|
} : void 0,
|
|
2318
|
+
resources: inspectResourceLimits(data.HostConfig),
|
|
2124
2319
|
composeProject: labels["com.docker.compose.project"] || void 0,
|
|
2125
2320
|
composeService: labels["com.docker.compose.service"] || void 0,
|
|
2126
2321
|
composeConfigFiles: configFiles ? configFiles.split(",").map((s) => s.trim()).filter(Boolean) : void 0,
|
|
@@ -2334,6 +2529,45 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
2334
2529
|
uptimeSeconds: uptimeSeconds && uptimeSeconds > 0 ? uptimeSeconds : void 0
|
|
2335
2530
|
};
|
|
2336
2531
|
}
|
|
2532
|
+
/**
|
|
2533
|
+
* One stabilization reading for the post-deploy watch. Unlike
|
|
2534
|
+
* `getContainerInfo` this keeps `restarting` distinct and carries
|
|
2535
|
+
* `RestartCount` / `ExitCode` / health — the only fields that separate a
|
|
2536
|
+
* container that is UP from one that is bouncing. A vanished container is a
|
|
2537
|
+
* `missing` sample (not a throw): that is a verdict, not a transport error.
|
|
2538
|
+
*/
|
|
2539
|
+
async sampleStability(containerId) {
|
|
2540
|
+
let data;
|
|
2541
|
+
try {
|
|
2542
|
+
data = await this.docker.getContainer(containerId).inspect();
|
|
2543
|
+
} catch (err) {
|
|
2544
|
+
if (isDockerNotFoundError(err)) {
|
|
2545
|
+
return {
|
|
2546
|
+
state: "missing",
|
|
2547
|
+
exitCode: null,
|
|
2548
|
+
restartCount: 0,
|
|
2549
|
+
health: null,
|
|
2550
|
+
errorLine: null,
|
|
2551
|
+
oomKilled: false,
|
|
2552
|
+
restartPolicy: null
|
|
2553
|
+
};
|
|
2554
|
+
}
|
|
2555
|
+
throw err;
|
|
2556
|
+
}
|
|
2557
|
+
const rawState = (data.State?.Status ?? "").toLowerCase().trim();
|
|
2558
|
+
const state = ["created", "running", "restarting", "paused", "exited", "dead", "removing"].includes(rawState) ? rawState : "unknown";
|
|
2559
|
+
const rawHealth = (data.State?.Health?.Status ?? "").toLowerCase().trim();
|
|
2560
|
+
const health = rawHealth === "healthy" || rawHealth === "unhealthy" || rawHealth === "starting" ? rawHealth : null;
|
|
2561
|
+
return {
|
|
2562
|
+
state,
|
|
2563
|
+
exitCode: typeof data.State?.ExitCode === "number" ? data.State.ExitCode : null,
|
|
2564
|
+
restartCount: typeof data.RestartCount === "number" ? data.RestartCount : 0,
|
|
2565
|
+
health,
|
|
2566
|
+
errorLine: data.State?.Error?.trim() ? data.State.Error.trim() : null,
|
|
2567
|
+
oomKilled: data.State?.OOMKilled === true,
|
|
2568
|
+
restartPolicy: data.HostConfig?.RestartPolicy?.Name ?? null
|
|
2569
|
+
};
|
|
2570
|
+
}
|
|
2337
2571
|
async getRuntimeLogs(containerId, tail) {
|
|
2338
2572
|
const container = this.docker.getContainer(containerId);
|
|
2339
2573
|
const buffer = await container.logs({
|
|
@@ -2807,7 +3041,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
2807
3041
|
...config.publicPort && config.environment.PORT === void 0 ? [`PORT=${config.publicPort}`] : [],
|
|
2808
3042
|
...Object.entries(config.environment).map(([k, v]) => `${k}=${v}`)
|
|
2809
3043
|
];
|
|
2810
|
-
const cmd = config
|
|
3044
|
+
const cmd = resolveComposeCmd(config);
|
|
2811
3045
|
const { exposedPorts, portBindings } = parsePortBindings(config.ports);
|
|
2812
3046
|
if (!config.namespaceVolumes) {
|
|
2813
3047
|
await this.assertNoForeignNamedVolumeCollision(config);
|
|
@@ -2819,6 +3053,12 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
2819
3053
|
log({
|
|
2820
3054
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2821
3055
|
message: `Creating service container ${containerName} from ${config.image}...
|
|
3056
|
+
`,
|
|
3057
|
+
level: "info"
|
|
3058
|
+
});
|
|
3059
|
+
log({
|
|
3060
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3061
|
+
message: `Resource limits: ${describeResourceLimits(config.resources)}
|
|
2822
3062
|
`,
|
|
2823
3063
|
level: "info"
|
|
2824
3064
|
});
|
|
@@ -2858,12 +3098,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
2858
3098
|
ExposedPorts: exposedPorts,
|
|
2859
3099
|
HostConfig: {
|
|
2860
3100
|
RestartPolicy: restartPolicy,
|
|
2861
|
-
...config.resources
|
|
2862
|
-
Memory: config.resources.memoryMb * 1024 * 1024
|
|
2863
|
-
},
|
|
2864
|
-
...config.resources?.cpuCores && {
|
|
2865
|
-
CpuShares: Math.round(config.resources.cpuCores * 1024)
|
|
2866
|
-
},
|
|
3101
|
+
...dockerResourceLimits(config.resources),
|
|
2867
3102
|
PortBindings: portBindings,
|
|
2868
3103
|
Binds: binds,
|
|
2869
3104
|
NetworkMode: group.id
|
|
@@ -2927,5 +3162,6 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
2927
3162
|
export {
|
|
2928
3163
|
scopedVolumeName,
|
|
2929
3164
|
isHostPathSource,
|
|
3165
|
+
ownsBuiltImage,
|
|
2930
3166
|
DockerRuntime
|
|
2931
3167
|
};
|
|
@@ -4,7 +4,7 @@ import { createRequire as __ospCreateRequire } from "node:module";
|
|
|
4
4
|
const require = __ospCreateRequire(import.meta.url);
|
|
5
5
|
import {
|
|
6
6
|
safeErrorMessage
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-UMQP7SYT.js";
|
|
8
8
|
|
|
9
9
|
// ../../packages/adapters/src/infra/openresty-lua.ts
|
|
10
10
|
import { createHash } from "crypto";
|
|
@@ -85,16 +85,26 @@ async function detectOpenRestyPaths(executor) {
|
|
|
85
85
|
pidPath
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
|
-
function buildReloadCommand(paths) {
|
|
88
|
+
function buildReloadCommand(paths, opts = {}) {
|
|
89
|
+
if (opts.containerEdge) {
|
|
90
|
+
return `${paths.bin} -t 2>&1 || exit 1
|
|
91
|
+
${paths.bin} -s reload 2>&1 || exit 1`;
|
|
92
|
+
}
|
|
89
93
|
return `${paths.bin} -t 2>&1 || exit 1
|
|
90
94
|
|
|
91
|
-
|
|
92
|
-
|
|
95
|
+
reload_err=$(${paths.bin} -s reload 2>&1) && exit 0
|
|
96
|
+
|
|
97
|
+
if [ "$(cat /proc/1/comm 2>/dev/null)" = "openresty" ] || [ "$(cat /proc/1/comm 2>/dev/null)" = "nginx" ]; then
|
|
98
|
+
echo "openresty reload failed and openresty is PID 1 (containerized edge) \u2014 not killing it; the container supervisor owns restarts." >&2
|
|
99
|
+
echo "$reload_err" >&2
|
|
100
|
+
exit 1
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
if [ -f ${paths.pidPath} ] && kill -0 "$(cat ${paths.pidPath} 2>/dev/null)" 2>/dev/null; then
|
|
104
|
+
echo "openresty (pid $(cat ${paths.pidPath})) is running but refused -s reload" >&2
|
|
105
|
+
exit 1
|
|
93
106
|
fi
|
|
94
107
|
|
|
95
|
-
pkill -f '[o]penresty' >/dev/null 2>&1 || true
|
|
96
|
-
pkill -f '[n]ginx' >/dev/null 2>&1 || true
|
|
97
|
-
sleep 1
|
|
98
108
|
rm -f ${paths.pidPath}
|
|
99
109
|
${paths.bin}`;
|
|
100
110
|
}
|
|
@@ -138,13 +148,18 @@ http {
|
|
|
138
148
|
default_type application/octet-stream;
|
|
139
149
|
sendfile on;
|
|
140
150
|
keepalive_timeout 65;
|
|
151
|
+
# See apps/edge/nginx.conf: the 64-byte default fails \`nginx -t\` outright
|
|
152
|
+
# ("could not build server_names_hash") for any server_name past ~63 chars,
|
|
153
|
+
# which refuses the reload and wedges every route on the box, not just the
|
|
154
|
+
# long one. Keep this in step with the containerized edge's value.
|
|
155
|
+
server_names_hash_bucket_size 128;
|
|
141
156
|
include ${sitesDir}/*.conf;
|
|
142
157
|
}
|
|
143
158
|
`;
|
|
144
159
|
}
|
|
145
160
|
var GEOIP_DIR = "/usr/share/GeoIP";
|
|
146
161
|
var GEOIP_DB_PATH = `${GEOIP_DIR}/GeoLite2-Country.mmdb`;
|
|
147
|
-
var GEOIP_DB_URL = "https://github.com/P3TERX/GeoLite.mmdb/releases/download/
|
|
162
|
+
var GEOIP_DB_URL = "https://github.com/P3TERX/GeoLite.mmdb/releases/latest/download/GeoLite2-Country.mmdb";
|
|
148
163
|
var LUA_SRC_DIR = process.env.OPENSHIP_LUA_DIR?.trim() || join(dirname(fileURLToPath(import.meta.url)), "lua");
|
|
149
164
|
var VHOST_REFERENCED_LUA = ["rules_guard.lua", "rules_lib.lua", "site_logger.lua"];
|
|
150
165
|
function luaSourceAvailable() {
|
|
@@ -5,12 +5,12 @@ const require = __ospCreateRequire(import.meta.url);
|
|
|
5
5
|
import {
|
|
6
6
|
getTarCreateEnv,
|
|
7
7
|
prepareSourceTarArgs
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-ZCOQJCZO.js";
|
|
9
9
|
import {
|
|
10
10
|
TRANSFER_EXCLUDES,
|
|
11
11
|
packageManagerEnsureCommand,
|
|
12
12
|
safeErrorMessage
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-UMQP7SYT.js";
|
|
14
14
|
|
|
15
15
|
// ../../packages/adapters/src/runtime/git-clone.ts
|
|
16
16
|
function sq(value) {
|