openship 0.6.6 → 0.6.7
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/dist/bare-7QD623QV.js +29 -0
- package/dist/{chunk-Q7WGTQQ5.js → chunk-2ANV5ZQ5.js} +1 -1
- package/dist/{chunk-UJ6JZM65.js → chunk-2VSW6Q6C.js} +601 -382
- package/dist/{chunk-PCPVTH6M.js → chunk-5T4UVOM3.js} +6 -4
- package/dist/{chunk-YMMQRE5J.js → chunk-BLEXTYRK.js} +1 -1
- package/dist/{chunk-OBPKYXQL.js → chunk-CIJV43LH.js} +71 -32
- package/dist/{chunk-NK5N5LXN.js → chunk-DDFUESPJ.js} +24 -9
- package/dist/{chunk-6FH4AXVY.js → chunk-E2X26PXR.js} +1 -1
- package/dist/{chunk-HKRQXPTI.js → chunk-FYPFEDPZ.js} +1 -1
- package/dist/{chunk-UVNMQM7W.js → chunk-FZ6QCF6B.js} +8 -6
- package/dist/{chunk-MLYSEZQB.js → chunk-GGIAIGUL.js} +15 -66
- package/dist/{chunk-2YM3NI2E.js → chunk-GIPJNNI5.js} +38 -44
- package/dist/{chunk-ZFJXPCQP.js → chunk-H3WT3A6R.js} +4 -3
- package/dist/chunk-H4FS5BED.js +936 -0
- package/dist/{chunk-CBLD2HS5.js → chunk-J2XM3QSP.js} +8 -5
- package/dist/{chunk-FE7ZZQSE.js → chunk-J3K6XABC.js} +3 -3
- package/dist/{chunk-77QDF6UD.js → chunk-LHFXSC36.js} +428 -93
- package/dist/{chunk-YIMOCZBZ.js → chunk-MEE6OZFP.js} +3 -3
- package/dist/chunk-N5YZMO33.js +111 -0
- package/dist/{chunk-QVW76WDL.js → chunk-NRTJRNAE.js} +58 -16
- package/dist/{chunk-WVAML4H6.js → chunk-PPEBIY5F.js} +247 -61
- package/dist/{chunk-4I2BM2DT.js → chunk-PVP4RGWT.js} +3 -3
- package/dist/{chunk-RSR7PFIQ.js → chunk-QHMDCYZ3.js} +4 -4
- package/dist/chunk-R4VDPFLS.js +106 -0
- package/dist/{chunk-VTGPEC6B.js → chunk-RDXT4BQB.js} +6 -4
- package/dist/{chunk-LFE5KQ7Y.js → chunk-S6B2Z3OO.js} +142 -57
- package/dist/chunk-UWEV6XTS.js +249 -0
- package/dist/{chunk-5DMWSTOM.js → chunk-VQM6JHJB.js} +940 -224
- package/dist/{chunk-SM26HDS2.js → chunk-VSTFHUCV.js} +44 -50
- package/dist/chunk-WU54BXFE.js +70 -0
- package/dist/{chunk-LUKYPVJ4.js → chunk-XWHWTHEL.js} +313 -469
- package/dist/{chunk-A4GXYE4L.js → chunk-ZPJ74LXE.js} +22 -20
- package/dist/cloud-KNNABE3O.js +40 -0
- package/dist/{detect-OIE5V3VZ.js → detect-G4DR3U2N.js} +9 -6
- package/dist/{docker-VTR2GK5Y.js → docker-YOVPCIB4.js} +11 -8
- package/dist/{docker-edge-executor-22IBXNYJ.js → docker-edge-executor-QFXD3337.js} +8 -6
- package/dist/edge-import-CRFVSR6P.js +56 -0
- package/dist/{edge-preflight-GBNCPNVU.js → edge-preflight-MXERVAMG.js} +15 -12
- package/dist/{ensure-container-edge-KPI2CVXJ.js → ensure-container-edge-F4JUNQIK.js} +13 -10
- package/dist/{executor-7LXXZPBR.js → executor-CVPAYYHR.js} +8 -6
- package/dist/host-channel-preflight-QTDNGLCK.js +54 -0
- package/dist/index.js +84 -48
- package/dist/{local-executor-WOFZD72X.js → local-executor-ODC6RANU.js} +3 -2
- package/dist/{nginx-KUFMLMAC.js → nginx-KLFF43TC.js} +11 -8
- package/dist/{openresty-lua-ATTH35BE.js → openresty-lua-VKWUV4GB.js} +6 -5
- package/dist/{privilege-I4W3ZNZK.js → privilege-7D3SSOL3.js} +6 -5
- package/dist/server/index.js +8813 -5232
- package/dist/setup-I5AJC6NJ.js +25 -0
- package/dist/{ssh-client-JBMA7SN4.js → ssh-client-625VWGKP.js} +3 -2
- package/dist/up-QXTOOVXC.js +61 -0
- package/package.json +1 -1
- package/dist/bare-OXJVW3AK.js +0 -25
- package/dist/chunk-LH2PLRQO.js +0 -434
- package/dist/cloud-75VIT4KV.js +0 -37
- package/dist/edge-import-WJUB4M7F.js +0 -52
- package/dist/host-channel-preflight-KE6OONYK.js +0 -50
- package/dist/setup-XGLGUUFW.js +0 -22
- package/dist/up-K4BV7466.js +0 -57
|
@@ -3,24 +3,37 @@
|
|
|
3
3
|
import { createRequire as __ospCreateRequire } from "node:module";
|
|
4
4
|
const require = __ospCreateRequire(import.meta.url);
|
|
5
5
|
import {
|
|
6
|
+
BuildKitTraceDecoder,
|
|
6
7
|
createDockerBuildContext,
|
|
7
|
-
|
|
8
|
+
missingContextDockerfileMessage,
|
|
8
9
|
prepareSourceTree,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
resolveServiceDockerfile
|
|
11
|
+
} from "./chunk-XWHWTHEL.js";
|
|
12
|
+
import {
|
|
13
|
+
isArtifactPathRef,
|
|
14
|
+
removeManagedArtifact
|
|
15
|
+
} from "./chunk-WU54BXFE.js";
|
|
13
16
|
import {
|
|
14
17
|
BuildCancelledError,
|
|
15
18
|
BuildLogger,
|
|
16
19
|
assembleGitClone,
|
|
20
|
+
builderContextRoot,
|
|
21
|
+
dockerBuildContextDirectory,
|
|
22
|
+
droppedRuntimeEnvMessage,
|
|
23
|
+
generateDockerfile,
|
|
24
|
+
isExcludedDocRootEntry,
|
|
17
25
|
killProcessesUnderDir,
|
|
18
26
|
materializeGitSsh,
|
|
27
|
+
normalizeDockerRelativePath,
|
|
19
28
|
parseLogLevel,
|
|
29
|
+
resolveContextDockerfileCandidates,
|
|
30
|
+
resolveDockerfileCandidates,
|
|
20
31
|
shellGitSshWriter,
|
|
32
|
+
splitRuntimeEnv,
|
|
21
33
|
sq,
|
|
34
|
+
staticBuilderOutputPath,
|
|
22
35
|
transferLocalDirectory
|
|
23
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-H4FS5BED.js";
|
|
24
37
|
import {
|
|
25
38
|
DOCKER_DIAL_STDIO_COMMAND,
|
|
26
39
|
attachDialStdioDiagnostics,
|
|
@@ -29,23 +42,28 @@ import {
|
|
|
29
42
|
execSshCommand,
|
|
30
43
|
openDockerDialStdioChannel,
|
|
31
44
|
openSshUnixSocket
|
|
32
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-5T4UVOM3.js";
|
|
33
46
|
import {
|
|
34
47
|
dirOf,
|
|
35
48
|
ensureOwnedDir
|
|
36
|
-
} from "./chunk-
|
|
49
|
+
} from "./chunk-FZ6QCF6B.js";
|
|
50
|
+
import {
|
|
51
|
+
OPENSHIP_LABEL
|
|
52
|
+
} from "./chunk-UWEV6XTS.js";
|
|
53
|
+
import {
|
|
54
|
+
isRuntimeNotFoundError
|
|
55
|
+
} from "./chunk-N5YZMO33.js";
|
|
37
56
|
import {
|
|
38
57
|
formatCpuCores,
|
|
39
58
|
formatMemoryMb,
|
|
40
59
|
hasCpuLimit,
|
|
41
60
|
hasMemoryLimit,
|
|
42
|
-
isRuntimeNotFoundError,
|
|
43
61
|
ownsNetworkEndpoint,
|
|
44
62
|
registryConfigKeys,
|
|
45
63
|
registryForImage,
|
|
46
64
|
safeErrorMessage,
|
|
47
65
|
withTimeout
|
|
48
|
-
} from "./chunk-
|
|
66
|
+
} from "./chunk-2VSW6Q6C.js";
|
|
49
67
|
import {
|
|
50
68
|
__commonJS,
|
|
51
69
|
__require,
|
|
@@ -4609,6 +4627,7 @@ var tarFs = __toESM(require_tar_fs(), 1);
|
|
|
4609
4627
|
import Dockerode from "dockerode";
|
|
4610
4628
|
import { createGzip } from "zlib";
|
|
4611
4629
|
import { PassThrough, Writable } from "stream";
|
|
4630
|
+
import { relative, sep } from "path";
|
|
4612
4631
|
|
|
4613
4632
|
// ../../packages/adapters/src/runtime/docker-auth.ts
|
|
4614
4633
|
import { readFile } from "fs/promises";
|
|
@@ -4712,6 +4731,10 @@ var MODE_SUFFIX = /:(ro|rw|z|Z|nocopy)$/;
|
|
|
4712
4731
|
function scopedVolumeName(slug, name) {
|
|
4713
4732
|
return `openship-${slug}-${name}`;
|
|
4714
4733
|
}
|
|
4734
|
+
function ensureScopedVolumeName(slug, source) {
|
|
4735
|
+
const prefix = `openship-${slug}-`;
|
|
4736
|
+
return source.startsWith(prefix) ? source : scopedVolumeName(slug, source);
|
|
4737
|
+
}
|
|
4715
4738
|
function isHostPathSource(source) {
|
|
4716
4739
|
return source.startsWith("/") || source.startsWith("./") || source.startsWith("../") || source.startsWith("~");
|
|
4717
4740
|
}
|
|
@@ -5701,9 +5724,10 @@ function parsePortBindings(portSpecs) {
|
|
|
5701
5724
|
}
|
|
5702
5725
|
return { exposedPorts, portBindings };
|
|
5703
5726
|
}
|
|
5704
|
-
function packBuildContext(contextDir, entries, cancelSignal) {
|
|
5727
|
+
function packBuildContext(contextDir, entries, cancelSignal, ignoreContextPath) {
|
|
5705
5728
|
const controller = new AbortController();
|
|
5706
|
-
const
|
|
5729
|
+
const ignore = ignoreContextPath ? (absolutePath) => ignoreContextPath(relative(contextDir, absolutePath).split(sep).join("/")) : void 0;
|
|
5730
|
+
const pack2 = tarFs.pack(contextDir, { entries, ignore });
|
|
5707
5731
|
const body = pack2.pipe(createGzip());
|
|
5708
5732
|
let contextError = null;
|
|
5709
5733
|
const capture = (err) => {
|
|
@@ -5835,14 +5859,22 @@ function extractNetworkInfo(data) {
|
|
|
5835
5859
|
break;
|
|
5836
5860
|
}
|
|
5837
5861
|
}
|
|
5862
|
+
const hostPortByContainerPort = {};
|
|
5838
5863
|
let hostPort;
|
|
5839
|
-
for (const bindings of Object.
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5864
|
+
for (const [key, bindings] of Object.entries(data.NetworkSettings.Ports ?? {})) {
|
|
5865
|
+
const bound = bindings?.[0]?.HostPort;
|
|
5866
|
+
if (!bound) continue;
|
|
5867
|
+
const parsed = parseInt(bound, 10);
|
|
5868
|
+
if (!Number.isFinite(parsed)) continue;
|
|
5869
|
+
const containerPort = parseInt(key.split("/")[0] ?? "", 10);
|
|
5870
|
+
if (Number.isFinite(containerPort)) hostPortByContainerPort[containerPort] = parsed;
|
|
5871
|
+
hostPort ??= parsed;
|
|
5844
5872
|
}
|
|
5845
|
-
return {
|
|
5873
|
+
return {
|
|
5874
|
+
ip,
|
|
5875
|
+
hostPort,
|
|
5876
|
+
...Object.keys(hostPortByContainerPort).length ? { hostPortByContainerPort } : {}
|
|
5877
|
+
};
|
|
5846
5878
|
}
|
|
5847
5879
|
function parseLoadedImageRef(output) {
|
|
5848
5880
|
const matches = [...(output || "").matchAll(/Loaded image(?: ID)?:\s*(\S+)/gi)];
|
|
@@ -5999,11 +6031,11 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
5999
6031
|
/** Labels applied to both build images and deploy containers. */
|
|
6000
6032
|
labels(config) {
|
|
6001
6033
|
const l = {
|
|
6002
|
-
|
|
6034
|
+
[OPENSHIP_LABEL.project]: config.projectId
|
|
6003
6035
|
};
|
|
6004
|
-
if (config.deploymentId) l[
|
|
6005
|
-
if (config.sessionId) l[
|
|
6006
|
-
if (config.service) l[
|
|
6036
|
+
if (config.deploymentId) l[OPENSHIP_LABEL.deployment] = config.deploymentId;
|
|
6037
|
+
if (config.sessionId) l[OPENSHIP_LABEL.build] = config.sessionId;
|
|
6038
|
+
if (config.service) l[OPENSHIP_LABEL.service] = config.service;
|
|
6007
6039
|
return l;
|
|
6008
6040
|
}
|
|
6009
6041
|
// ── Build lifecycle ────────────────────────────────────────────────────
|
|
@@ -6045,12 +6077,20 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6045
6077
|
logger.log(`[${step}] ${label} ${message}`);
|
|
6046
6078
|
logger.step(step, status, message);
|
|
6047
6079
|
}
|
|
6048
|
-
handleBuildEvent(event, logger) {
|
|
6080
|
+
handleBuildEvent(event, logger, trace) {
|
|
6049
6081
|
const errorMessage = event.errorDetail?.message ?? event.error;
|
|
6050
6082
|
if (errorMessage) {
|
|
6051
6083
|
logger.log(errorMessage, "error");
|
|
6052
6084
|
return errorMessage;
|
|
6053
6085
|
}
|
|
6086
|
+
if (event.id === "moby.buildkit.trace" && typeof event.aux === "string") {
|
|
6087
|
+
let hint = null;
|
|
6088
|
+
for (const line of trace?.push(event.aux) ?? []) {
|
|
6089
|
+
logger.log(line.message, line.level === "error" ? "error" : parseLogLevel(line.message));
|
|
6090
|
+
hint ??= this.extractBuildFailureHint(line.message);
|
|
6091
|
+
}
|
|
6092
|
+
return hint;
|
|
6093
|
+
}
|
|
6054
6094
|
if (event.stream) {
|
|
6055
6095
|
const line = event.stream.trim();
|
|
6056
6096
|
if (!line) return null;
|
|
@@ -6107,6 +6147,12 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6107
6147
|
if (/returned a non-zero code:\s*\d+/i.test(line)) {
|
|
6108
6148
|
return line;
|
|
6109
6149
|
}
|
|
6150
|
+
if (/requires? BuildKit|BuildKit is enabled but the buildx component/i.test(line)) {
|
|
6151
|
+
return `${line} \u2014 this Dockerfile needs BuildKit. Install the docker buildx plugin on the build host (\`docker-buildx-plugin\`), or remove the BuildKit-only instruction.`;
|
|
6152
|
+
}
|
|
6153
|
+
if (/buildkit (is )?(not (supported|enabled)|disabled)|builder version.*not supported/i.test(line)) {
|
|
6154
|
+
return `${line} \u2014 this Dockerfile needs BuildKit but this Docker daemon has it disabled. Enable it ("features": {"buildkit": true} in daemon.json) or remove the BuildKit-only instruction.`;
|
|
6155
|
+
}
|
|
6110
6156
|
if (/\/workspace\/package\.json/i.test(line) && /ENOENT/i.test(line)) {
|
|
6111
6157
|
return "Docker build ran from /workspace but package.json was not found there. The configured rootDirectory is likely empty or incorrect.";
|
|
6112
6158
|
}
|
|
@@ -6158,12 +6204,35 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6158
6204
|
{ excludes: [] }
|
|
6159
6205
|
);
|
|
6160
6206
|
}
|
|
6207
|
+
/**
|
|
6208
|
+
* Is BuildKit usable on the remote host? Probed, never assumed: a plain `docker`
|
|
6209
|
+
* CLI without the buildx plugin HARD-FAILS when BuildKit is requested explicitly
|
|
6210
|
+
* ("BuildKit is enabled but the buildx component is missing or broken"), while an
|
|
6211
|
+
* unadorned `docker build` on the same box falls back to the legacy builder and
|
|
6212
|
+
* works — the same trap {@link buildImage} in system/managed-image.ts documents.
|
|
6213
|
+
* So we ask first, and only then opt in.
|
|
6214
|
+
*
|
|
6215
|
+
* Memoized per adapter instance (one instance ≈ one target daemon), so a compose
|
|
6216
|
+
* batch pays a single `docker buildx version` for N images.
|
|
6217
|
+
*/
|
|
6218
|
+
buildKitProbe = null;
|
|
6219
|
+
async remoteBuildKitAvailable() {
|
|
6220
|
+
const executor = this.connectionOptions?.executor;
|
|
6221
|
+
if (!executor) return false;
|
|
6222
|
+
const probe = this.buildKitProbe ??= executor.exec("docker buildx version >/dev/null 2>&1 && echo OPENSHIP_BUILDKIT_OK || true").then((out) => out.split(/\r?\n/).some((line) => line.trim() === "OPENSHIP_BUILDKIT_OK")).catch(() => false);
|
|
6223
|
+
const available = await probe;
|
|
6224
|
+
if (!available) this.buildKitProbe = null;
|
|
6225
|
+
return available;
|
|
6226
|
+
}
|
|
6161
6227
|
/**
|
|
6162
6228
|
* Run native `docker build` on the remote host against an already-transferred
|
|
6163
|
-
* context dir. One image; `dockerfileName` selects which Dockerfile
|
|
6164
|
-
*
|
|
6229
|
+
* context dir. One image; `dockerfileName` selects which Dockerfile to use (so N
|
|
6230
|
+
* services can build from one transferred tree) and is relative to
|
|
6231
|
+
* `remoteBuildDir` — the build CONTEXT, which for a compose service is its own
|
|
6232
|
+
* `build:` directory inside that tree, not the tree root (#634).
|
|
6165
6233
|
*/
|
|
6166
|
-
async buildImageOnRemote(config,
|
|
6234
|
+
async buildImageOnRemote(config, remoteBuildDir, dockerfileName, tag, log, opts) {
|
|
6235
|
+
const signal = opts?.signal;
|
|
6167
6236
|
const executor = this.connectionOptions?.executor;
|
|
6168
6237
|
if (!executor) throw new Error("SSH build path requires an executor on connectionOptions");
|
|
6169
6238
|
const buildArgs = Object.entries({
|
|
@@ -6174,7 +6243,22 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6174
6243
|
this.labels({ projectId: config.projectId, sessionId: config.sessionId })
|
|
6175
6244
|
).map(([k, v]) => `--label ${sq(`${k}=${v}`)}`).join(" ");
|
|
6176
6245
|
const dockerfileFlag = dockerfileName && dockerfileName !== "Dockerfile" ? ` -f ${sq(dockerfileName)}` : "";
|
|
6177
|
-
const
|
|
6246
|
+
const buildKit = await this.remoteBuildKitAvailable();
|
|
6247
|
+
const builderEnv = buildKit ? "DOCKER_BUILDKIT=1 " : "";
|
|
6248
|
+
const builderFlags = buildKit ? " --progress=plain" : " --force-rm";
|
|
6249
|
+
if (buildKit) {
|
|
6250
|
+
log.log("Builder: BuildKit (docker buildx detected on the host)");
|
|
6251
|
+
} else if (opts?.requiresBuildKit) {
|
|
6252
|
+
log.log(
|
|
6253
|
+
"This Dockerfile needs BuildKit, but the build host's docker CLI has no buildx plugin. Install it (`docker-buildx-plugin`, or Docker's official packages) \u2014 the build below will use the legacy builder and is likely to fail.",
|
|
6254
|
+
"warn"
|
|
6255
|
+
);
|
|
6256
|
+
} else {
|
|
6257
|
+
log.log(
|
|
6258
|
+
"Builder: legacy (docker buildx not available on the host) \u2014 BuildKit-only Dockerfile syntax such as `RUN --mount` will not work"
|
|
6259
|
+
);
|
|
6260
|
+
}
|
|
6261
|
+
const buildCmd = `cd ${sq(remoteBuildDir)} && ${builderEnv}docker build${builderFlags} -t ${sq(tag)}${dockerfileFlag} ${labelArgs} ${buildArgs} .`;
|
|
6178
6262
|
log.log(`Running on remote: ${buildCmd}`);
|
|
6179
6263
|
log.log("\u2500\u2500\u2500 docker build output \u2500\u2500\u2500");
|
|
6180
6264
|
this.emitDockerStep(log, "install", "running", "Running install inside container (docker build)");
|
|
@@ -6286,15 +6370,50 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6286
6370
|
* (clone-on-server). Mirrors resolveServiceDockerfile but probes the remote
|
|
6287
6371
|
* tree with `test -f` instead of the local FS: a repository Dockerfile
|
|
6288
6372
|
* candidate is used when present; otherwise a Dockerfile is generated locally
|
|
6289
|
-
* (pure fn) and written to the remote tree.
|
|
6290
|
-
*
|
|
6373
|
+
* (pure fn) and written to the remote tree.
|
|
6374
|
+
*
|
|
6375
|
+
* Returns the build CONTEXT dir on the host plus the Dockerfile path relative to
|
|
6376
|
+
* it — the same pair {@link resolveServiceDockerfile} produces locally, because
|
|
6377
|
+
* these two resolvers are one behaviour with two probes and a narrowed context
|
|
6378
|
+
* learned by only one of them would leave #634 reproducible on this target.
|
|
6291
6379
|
*/
|
|
6292
|
-
async resolveRemoteDockerfile(config, remoteContextDir, generatedName, requireRepositoryDockerfile) {
|
|
6380
|
+
async resolveRemoteDockerfile(config, remoteContextDir, generatedName, requireRepositoryDockerfile, log) {
|
|
6293
6381
|
const executor = this.connectionOptions?.executor;
|
|
6294
6382
|
if (!executor) throw new Error("Clone-on-server requires an SSH executor on connectionOptions");
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6383
|
+
const contextSubdir = dockerBuildContextDirectory(config);
|
|
6384
|
+
const remoteBuildDir = contextSubdir ? `${remoteContextDir}/${contextSubdir}` : remoteContextDir;
|
|
6385
|
+
if (contextSubdir) {
|
|
6386
|
+
const real = await executor.exec(`cd ${sq(remoteBuildDir)} 2>/dev/null && pwd -P || true`).then((out) => out.trim()).catch(() => "");
|
|
6387
|
+
const root = await executor.exec(`cd ${sq(remoteContextDir)} && pwd -P`).then((out) => out.trim()).catch(() => "");
|
|
6388
|
+
if (!real) {
|
|
6389
|
+
throw new Error(
|
|
6390
|
+
`Build context "${contextSubdir}" is not a directory in the cloned source. Check the service's build path.`
|
|
6391
|
+
);
|
|
6392
|
+
}
|
|
6393
|
+
if (!root || real !== root && !real.startsWith(`${root}/`)) {
|
|
6394
|
+
throw new Error(`Build context "${contextSubdir}" resolves outside the cloned source.`);
|
|
6395
|
+
}
|
|
6396
|
+
}
|
|
6397
|
+
const candidates = contextSubdir ? resolveContextDockerfileCandidates(contextSubdir, config.dockerfilePath) : resolveDockerfileCandidates(config.rootDirectory, config.dockerfilePath);
|
|
6398
|
+
for (const candidate of candidates) {
|
|
6399
|
+
const out = await executor.exec(`test -f ${sq(`${remoteBuildDir}/${candidate}`)} && echo yes || true`).catch(() => "");
|
|
6400
|
+
if (out.trim() === "yes") {
|
|
6401
|
+
const explicit = normalizeDockerRelativePath(config.dockerfilePath);
|
|
6402
|
+
if (contextSubdir && explicit && candidate === "Dockerfile" && explicit !== "Dockerfile" && explicit !== `${contextSubdir}/Dockerfile`) {
|
|
6403
|
+
log?.log(
|
|
6404
|
+
`Configured Dockerfile "${config.dockerfilePath}" was not found in build context "${contextSubdir}" \u2014 using ${contextSubdir}/Dockerfile instead.`,
|
|
6405
|
+
"warn"
|
|
6406
|
+
);
|
|
6407
|
+
}
|
|
6408
|
+
return { remoteBuildDir, dockerfileName: candidate };
|
|
6409
|
+
}
|
|
6410
|
+
}
|
|
6411
|
+
if (contextSubdir) {
|
|
6412
|
+
throw new Error(
|
|
6413
|
+
missingContextDockerfileMessage(contextSubdir, config.dockerfilePath, {
|
|
6414
|
+
generatedRecipeRefused: !requireRepositoryDockerfile
|
|
6415
|
+
})
|
|
6416
|
+
);
|
|
6298
6417
|
}
|
|
6299
6418
|
if (requireRepositoryDockerfile) {
|
|
6300
6419
|
const expected = config.dockerfilePath?.trim() || "Dockerfile";
|
|
@@ -6303,7 +6422,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6303
6422
|
);
|
|
6304
6423
|
}
|
|
6305
6424
|
await executor.writeFile(`${remoteContextDir}/${generatedName}`, generateDockerfile(config));
|
|
6306
|
-
return generatedName;
|
|
6425
|
+
return { remoteBuildDir, dockerfileName: generatedName };
|
|
6307
6426
|
}
|
|
6308
6427
|
async buildViaSshTarPipe(config, buildContext, tag, log, signal) {
|
|
6309
6428
|
const remoteContextDir = `/tmp/openship-build-${config.sessionId}`;
|
|
@@ -6311,11 +6430,11 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6311
6430
|
await this.transferBuildContext(buildContext.contextDir, remoteContextDir, log);
|
|
6312
6431
|
await this.buildImageOnRemote(
|
|
6313
6432
|
config,
|
|
6314
|
-
remoteContextDir,
|
|
6433
|
+
buildContext.contextSubdir ? `${remoteContextDir}/${buildContext.contextSubdir}` : remoteContextDir,
|
|
6315
6434
|
buildContext.dockerfileName,
|
|
6316
6435
|
tag,
|
|
6317
6436
|
log,
|
|
6318
|
-
signal
|
|
6437
|
+
{ signal, requiresBuildKit: buildContext.requiresBuildKit }
|
|
6319
6438
|
);
|
|
6320
6439
|
} finally {
|
|
6321
6440
|
this.connectionOptions?.executor?.exec(`rm -rf ${sq(remoteContextDir)}`).catch(() => {
|
|
@@ -6323,6 +6442,41 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6323
6442
|
await buildContext.cleanup();
|
|
6324
6443
|
}
|
|
6325
6444
|
}
|
|
6445
|
+
/**
|
|
6446
|
+
* Pick the Engine-API builder for ONE dockerode build, for BOTH dockerode call
|
|
6447
|
+
* sites (single image and compose batch) — they are the same decision and a
|
|
6448
|
+
* builder learned by only one of them is the "two executors" trap all over again.
|
|
6449
|
+
*
|
|
6450
|
+
* The classic builder is kept as the DEFAULT deliberately. It is what every
|
|
6451
|
+
* self-hosted local build has always used; switching wholesale would change layer
|
|
6452
|
+
* caching for everyone and silently drop the build CPU/memory caps, which BuildKit
|
|
6453
|
+
* ignores. So BuildKit is opted into only where the classic builder cannot work at
|
|
6454
|
+
* all — a Dockerfile whose syntax it rejects (#634).
|
|
6455
|
+
*
|
|
6456
|
+
* Through the ENGINE API (`version=2`), not by shelling out to `docker build`,
|
|
6457
|
+
* which is the first alternative that comes to mind and is not available: the API
|
|
6458
|
+
* image ships `openssh-client rsync git` and no docker CLI (apps/api/Dockerfile),
|
|
6459
|
+
* and the transport may be a TCP socket with no shell at the other end at all.
|
|
6460
|
+
* Nothing needs the buildx plugin either — BuildKit lives in the daemon.
|
|
6461
|
+
*
|
|
6462
|
+
* Asymmetric with the remote CLI path on purpose: there, BuildKit is taken whenever
|
|
6463
|
+
* buildx exists, because that command passes no --memory/--cpus (so there is no cap
|
|
6464
|
+
* to lose) and on docker ≥23 the CLI already defaults to it — the prefix mostly
|
|
6465
|
+
* buys the log line and the buildx-missing warning.
|
|
6466
|
+
*/
|
|
6467
|
+
selectDockerodeBuilder(config, requiresBuildKit, log) {
|
|
6468
|
+
if (!requiresBuildKit) return { options: {} };
|
|
6469
|
+
log.log(
|
|
6470
|
+
"Dockerfile uses BuildKit-only syntax (RUN --mount, a syntax directive or a heredoc) \u2014 building with BuildKit."
|
|
6471
|
+
);
|
|
6472
|
+
if (Object.keys(dockerBuildResourceLimits(config.resources)).length > 0) {
|
|
6473
|
+
log.log(
|
|
6474
|
+
"Build CPU/memory limits are not enforced under BuildKit \u2014 this build runs uncapped.",
|
|
6475
|
+
"warn"
|
|
6476
|
+
);
|
|
6477
|
+
}
|
|
6478
|
+
return { options: { version: "2" }, trace: new BuildKitTraceDecoder() };
|
|
6479
|
+
}
|
|
6326
6480
|
/**
|
|
6327
6481
|
* Dockerode build path. Used for local socket and TCP transports, plus
|
|
6328
6482
|
* SSH transports that didn't get an executor wired in (shouldn't happen
|
|
@@ -6335,10 +6489,12 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6335
6489
|
async buildViaDockerode(config, buildContext, tag, log, cancelSignal) {
|
|
6336
6490
|
log.log(`Streaming build context to Docker daemon - image tag: ${tag}`);
|
|
6337
6491
|
const { body, abortSignal, takeError } = packBuildContext(
|
|
6338
|
-
buildContext.
|
|
6492
|
+
buildContext.buildContextDir,
|
|
6339
6493
|
buildContext.contextEntries,
|
|
6340
|
-
cancelSignal
|
|
6494
|
+
cancelSignal,
|
|
6495
|
+
buildContext.ignoreContextPath
|
|
6341
6496
|
);
|
|
6497
|
+
const builder = this.selectDockerodeBuilder(config, buildContext.requiresBuildKit, log);
|
|
6342
6498
|
try {
|
|
6343
6499
|
const stream = await this.docker.buildImage(body, {
|
|
6344
6500
|
t: tag,
|
|
@@ -6353,10 +6509,11 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6353
6509
|
// needs several GB). Opt-in only; see dockerBuildResourceLimits.
|
|
6354
6510
|
...dockerBuildResourceLimits(config.resources),
|
|
6355
6511
|
forcerm: true,
|
|
6512
|
+
...builder.options,
|
|
6356
6513
|
abortSignal
|
|
6357
6514
|
});
|
|
6358
6515
|
log.log("Connected to Docker daemon. Build output follows:");
|
|
6359
|
-
await this.streamDockerodeBuild(stream, log);
|
|
6516
|
+
await this.streamDockerodeBuild(stream, log, builder.trace);
|
|
6360
6517
|
log.log("Docker daemon finished streaming build output. Finalizing image...\n");
|
|
6361
6518
|
} catch (err) {
|
|
6362
6519
|
const contextErr = takeError();
|
|
@@ -6370,7 +6527,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6370
6527
|
* idle timeout + keepalive, and throw on a fatal build event. Shared by the
|
|
6371
6528
|
* single-image dockerode path and the local batch build (buildImages).
|
|
6372
6529
|
*/
|
|
6373
|
-
async streamDockerodeBuild(stream, log) {
|
|
6530
|
+
async streamDockerodeBuild(stream, log, trace) {
|
|
6374
6531
|
let fatalBuildError = null;
|
|
6375
6532
|
await new Promise((resolve, reject) => {
|
|
6376
6533
|
let settled = false;
|
|
@@ -6427,7 +6584,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6427
6584
|
},
|
|
6428
6585
|
(event) => {
|
|
6429
6586
|
resetIdleTimer();
|
|
6430
|
-
fatalBuildError ??= this.handleBuildEvent(event, log);
|
|
6587
|
+
fatalBuildError ??= this.handleBuildEvent(event, log, trace);
|
|
6431
6588
|
}
|
|
6432
6589
|
);
|
|
6433
6590
|
});
|
|
@@ -6496,20 +6653,16 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6496
6653
|
await this.cloneSourceOnRemote(config, remoteContextDir, log);
|
|
6497
6654
|
if (abort.signal.aborted) throw new BuildCancelledError();
|
|
6498
6655
|
this.emitDockerStep(log, "clone", "completed", "Source cloned on the server");
|
|
6499
|
-
const dockerfileName = await this.resolveRemoteDockerfile(
|
|
6656
|
+
const { remoteBuildDir, dockerfileName } = await this.resolveRemoteDockerfile(
|
|
6500
6657
|
config,
|
|
6501
6658
|
remoteContextDir,
|
|
6502
6659
|
"Dockerfile.openship",
|
|
6503
|
-
config.stack === "docker"
|
|
6504
|
-
|
|
6505
|
-
await this.buildImageOnRemote(
|
|
6506
|
-
config,
|
|
6507
|
-
remoteContextDir,
|
|
6508
|
-
dockerfileName,
|
|
6509
|
-
tag,
|
|
6510
|
-
log,
|
|
6511
|
-
abort.signal
|
|
6660
|
+
config.stack === "docker",
|
|
6661
|
+
log
|
|
6512
6662
|
);
|
|
6663
|
+
await this.buildImageOnRemote(config, remoteBuildDir, dockerfileName, tag, log, {
|
|
6664
|
+
signal: abort.signal
|
|
6665
|
+
});
|
|
6513
6666
|
} finally {
|
|
6514
6667
|
sshExecutor.exec(`rm -rf ${sq(remoteContextDir)}`).catch(() => {
|
|
6515
6668
|
});
|
|
@@ -6539,7 +6692,9 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6539
6692
|
} catch {
|
|
6540
6693
|
this.emitDockerStep(log, "clone", "completed", "Docker build context ready");
|
|
6541
6694
|
}
|
|
6542
|
-
if (buildContext.
|
|
6695
|
+
if (buildContext.contextSubdir) {
|
|
6696
|
+
log.log(`Using Docker build context: ${buildContext.contextSubdir}`);
|
|
6697
|
+
} else if (buildContext.rootDirectory) {
|
|
6543
6698
|
log.log(`Using Docker build root: ${buildContext.rootDirectory}`);
|
|
6544
6699
|
}
|
|
6545
6700
|
if (buildContext.usesRepositoryDockerfile) {
|
|
@@ -6608,6 +6763,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6608
6763
|
await this.extractDocRootViaDaemon(tag, docRoot, hostOutDir);
|
|
6609
6764
|
}
|
|
6610
6765
|
}
|
|
6766
|
+
await this.pruneBuildFilesFromDocRoot(config, hostOutDir, sshExecutor ?? null, log);
|
|
6611
6767
|
log.log(`Static files ready at ${hostOutDir}
|
|
6612
6768
|
`);
|
|
6613
6769
|
} finally {
|
|
@@ -6692,6 +6848,74 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6692
6848
|
* expensive one: every extractor can succeed having copied nothing, which deploys
|
|
6693
6849
|
* green and then 404s the entire site.
|
|
6694
6850
|
*/
|
|
6851
|
+
/**
|
|
6852
|
+
* Names that must never end up in a public doc-root, matched at its TOP LEVEL
|
|
6853
|
+
* only.
|
|
6854
|
+
*
|
|
6855
|
+
* `.git` is the repository — serving it publishes full source history. The
|
|
6856
|
+
* Dockerfiles and `.dockerignore` are OUR build inputs, and one of them is
|
|
6857
|
+
* literally written into the tree by `resolveServiceDockerfile`.
|
|
6858
|
+
*/
|
|
6859
|
+
static DOC_ROOT_EXCLUDED = [
|
|
6860
|
+
".dockerignore",
|
|
6861
|
+
".git",
|
|
6862
|
+
"Dockerfile",
|
|
6863
|
+
"Dockerfile.*"
|
|
6864
|
+
];
|
|
6865
|
+
/**
|
|
6866
|
+
* Remove build inputs from an extracted static doc-root, then re-assert the
|
|
6867
|
+
* contract that it still holds something servable.
|
|
6868
|
+
*
|
|
6869
|
+
* Only runs when the doc-root IS the build context — `outputDirectory` empty or
|
|
6870
|
+
* "." — which is the zero-build "just files, index.html at the root" project. A
|
|
6871
|
+
* real output directory (`dist`, `build`) is written by the build and contains
|
|
6872
|
+
* none of this, so the prune is a no-op there.
|
|
6873
|
+
*
|
|
6874
|
+
* Two distinct problems, one fix. The build context is pruned by `.dockerignore`
|
|
6875
|
+
* and THEN the generated recipe is written into it, so for these projects the
|
|
6876
|
+
* published site contained `.dockerignore` + `Dockerfile.openship…` — and any
|
|
6877
|
+
* tracked `.git` — at their own public URLs. Worse, those files count as
|
|
6878
|
+
* content: a repo with an allowlist `.dockerignore` (`*` + `!src`) extracted to
|
|
6879
|
+
* a tree holding ONLY the two build files, which passed the non-empty check and
|
|
6880
|
+
* deployed green as a site that 404s every request. Pruning first and
|
|
6881
|
+
* re-checking after is what makes that check mean "there is something to serve".
|
|
6882
|
+
*/
|
|
6883
|
+
async pruneBuildFilesFromDocRoot(config, hostOutDir, sshExecutor, log) {
|
|
6884
|
+
if (staticBuilderOutputPath(config) !== builderContextRoot(config)) return;
|
|
6885
|
+
if (sshExecutor) {
|
|
6886
|
+
const globs = _DockerRuntime.DOC_ROOT_EXCLUDED.map(
|
|
6887
|
+
(name) => `${sq(hostOutDir)}/${name}`
|
|
6888
|
+
).join(" ");
|
|
6889
|
+
await sshExecutor.exec(`rm -rf ${globs}`).catch(() => {
|
|
6890
|
+
});
|
|
6891
|
+
log.log(`Excluded build files from the published output (${hostOutDir}).
|
|
6892
|
+
`);
|
|
6893
|
+
const listing = (await sshExecutor.exec(`ls -A ${sq(hostOutDir)} | head -1`).catch(() => "x")).trim();
|
|
6894
|
+
this.assertDocRootServable(!listing, hostOutDir);
|
|
6895
|
+
return;
|
|
6896
|
+
}
|
|
6897
|
+
const { readdir, rm } = await import("fs/promises");
|
|
6898
|
+
const { join: join3 } = await import("path");
|
|
6899
|
+
const entries = await readdir(hostOutDir).catch(() => null);
|
|
6900
|
+
if (entries === null) return;
|
|
6901
|
+
const doomed = entries.filter((name) => isExcludedDocRootEntry(name));
|
|
6902
|
+
for (const name of doomed) {
|
|
6903
|
+
await rm(join3(hostOutDir, name), { recursive: true, force: true }).catch(() => {
|
|
6904
|
+
});
|
|
6905
|
+
}
|
|
6906
|
+
if (doomed.length > 0) {
|
|
6907
|
+
log.log(`Excluded build files from the published output: ${doomed.join(", ")}
|
|
6908
|
+
`);
|
|
6909
|
+
}
|
|
6910
|
+
this.assertDocRootServable(entries.length === doomed.length, hostOutDir);
|
|
6911
|
+
}
|
|
6912
|
+
/** Post-prune half of the doc-root contract: build inputs are not content. */
|
|
6913
|
+
assertDocRootServable(isEmpty, hostOutDir) {
|
|
6914
|
+
if (!isEmpty) return;
|
|
6915
|
+
throw new Error(
|
|
6916
|
+
`the static output at ${hostOutDir} contains only build files (Dockerfile / .dockerignore) and nothing to serve \u2014 check the project's output directory setting and its .dockerignore.`
|
|
6917
|
+
);
|
|
6918
|
+
}
|
|
6695
6919
|
assertDocRootFilled(isEmpty, docRoot) {
|
|
6696
6920
|
if (!isEmpty) return;
|
|
6697
6921
|
throw new Error(
|
|
@@ -6878,7 +7102,25 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6878
7102
|
prepareLogger.step("clone", "completed", "Source cloned on the server");
|
|
6879
7103
|
} else {
|
|
6880
7104
|
prepareLogger.step("clone", "running", "Preparing shared build context...");
|
|
6881
|
-
tree = await prepareSourceTree(source, {
|
|
7105
|
+
tree = await prepareSourceTree(source, {
|
|
7106
|
+
onLog: prepareLogger.callback,
|
|
7107
|
+
// Any service building from its OWN `build:` directory makes the source
|
|
7108
|
+
// root's `.dockerignore` the wrong file to prune this SHARED tree with —
|
|
7109
|
+
// it would delete files the other contexts need. Those builds filter their
|
|
7110
|
+
// own context instead; see SourceTree.dockerignorePruned.
|
|
7111
|
+
//
|
|
7112
|
+
// A malformed value throws here; `true` is the safe answer for it, because
|
|
7113
|
+
// it only skips a destructive prune. Letting the throw out would fail the
|
|
7114
|
+
// WHOLE batch, while per-spec resolution below reports it against the one
|
|
7115
|
+
// service that owns it.
|
|
7116
|
+
perServiceBuildContexts: specs.some((spec) => {
|
|
7117
|
+
try {
|
|
7118
|
+
return Boolean(dockerBuildContextDirectory(spec.config));
|
|
7119
|
+
} catch {
|
|
7120
|
+
return true;
|
|
7121
|
+
}
|
|
7122
|
+
})
|
|
7123
|
+
});
|
|
6882
7124
|
}
|
|
6883
7125
|
const resolvedList = await Promise.all(
|
|
6884
7126
|
specs.map(async (spec) => {
|
|
@@ -6886,34 +7128,54 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6886
7128
|
const requireRepo = spec.requireRepositoryDockerfile ?? spec.config.stack === "docker";
|
|
6887
7129
|
try {
|
|
6888
7130
|
if (cloneOnServer) {
|
|
6889
|
-
const dockerfileName = await this.resolveRemoteDockerfile(
|
|
7131
|
+
const { remoteBuildDir, dockerfileName } = await this.resolveRemoteDockerfile(
|
|
6890
7132
|
spec.config,
|
|
6891
7133
|
remoteContextDir,
|
|
6892
7134
|
generatedName,
|
|
6893
|
-
requireRepo
|
|
7135
|
+
requireRepo,
|
|
7136
|
+
spec.logger
|
|
6894
7137
|
);
|
|
6895
7138
|
return {
|
|
6896
7139
|
spec,
|
|
6897
7140
|
dockerfileName,
|
|
7141
|
+
remoteBuildSubdir: dockerBuildContextDirectory(spec.config),
|
|
7142
|
+
remoteBuildDir,
|
|
7143
|
+
buildContextDir: null,
|
|
6898
7144
|
contextEntries: null,
|
|
7145
|
+
ignoreContextPath: void 0,
|
|
7146
|
+
// Only the dockerode branch consults it, and clone-on-server never
|
|
7147
|
+
// reaches that branch (it is SSH by construction).
|
|
7148
|
+
requiresBuildKit: false,
|
|
6899
7149
|
error: null
|
|
6900
7150
|
};
|
|
6901
7151
|
}
|
|
6902
7152
|
const resolved = await resolveServiceDockerfile(tree.contextDir, spec.config, {
|
|
6903
7153
|
requireRepositoryDockerfile: requireRepo,
|
|
6904
|
-
generatedName
|
|
7154
|
+
generatedName,
|
|
7155
|
+
dockerignorePruned: tree.dockerignorePruned,
|
|
7156
|
+
onLog: spec.logger.callback
|
|
6905
7157
|
});
|
|
6906
7158
|
return {
|
|
6907
7159
|
spec,
|
|
6908
7160
|
dockerfileName: resolved.dockerfileName,
|
|
7161
|
+
remoteBuildSubdir: resolved.contextSubdir,
|
|
7162
|
+
remoteBuildDir: resolved.contextSubdir ? `${remoteContextDir}/${resolved.contextSubdir}` : remoteContextDir,
|
|
7163
|
+
buildContextDir: resolved.buildContextDir,
|
|
6909
7164
|
contextEntries: resolved.contextEntries,
|
|
7165
|
+
ignoreContextPath: resolved.ignoreContextPath,
|
|
7166
|
+
requiresBuildKit: resolved.requiresBuildKit,
|
|
6910
7167
|
error: null
|
|
6911
7168
|
};
|
|
6912
7169
|
} catch (err) {
|
|
6913
7170
|
return {
|
|
6914
7171
|
spec,
|
|
6915
7172
|
dockerfileName: null,
|
|
7173
|
+
remoteBuildSubdir: "",
|
|
7174
|
+
remoteBuildDir: remoteContextDir,
|
|
7175
|
+
buildContextDir: null,
|
|
6916
7176
|
contextEntries: null,
|
|
7177
|
+
ignoreContextPath: void 0,
|
|
7178
|
+
requiresBuildKit: false,
|
|
6917
7179
|
error: safeErrorMessage(err)
|
|
6918
7180
|
};
|
|
6919
7181
|
}
|
|
@@ -6935,7 +7197,17 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6935
7197
|
}
|
|
6936
7198
|
}
|
|
6937
7199
|
const results = [];
|
|
6938
|
-
for (const {
|
|
7200
|
+
for (const {
|
|
7201
|
+
spec,
|
|
7202
|
+
dockerfileName,
|
|
7203
|
+
remoteBuildDir,
|
|
7204
|
+
remoteBuildSubdir,
|
|
7205
|
+
buildContextDir,
|
|
7206
|
+
contextEntries,
|
|
7207
|
+
ignoreContextPath,
|
|
7208
|
+
requiresBuildKit,
|
|
7209
|
+
error
|
|
7210
|
+
} of resolvedList) {
|
|
6939
7211
|
const startedAt = Date.now();
|
|
6940
7212
|
const tag = this.imageTag(spec.config.slug, spec.config.sessionId);
|
|
6941
7213
|
const signal = abortControllers.get(spec.config.sessionId)?.signal;
|
|
@@ -6958,20 +7230,30 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6958
7230
|
}
|
|
6959
7231
|
spec.onStart?.();
|
|
6960
7232
|
try {
|
|
7233
|
+
if (remoteBuildSubdir) {
|
|
7234
|
+
spec.logger.log(`Using Docker build context: ${remoteBuildSubdir}
|
|
7235
|
+
`);
|
|
7236
|
+
}
|
|
6961
7237
|
if (isSsh) {
|
|
6962
7238
|
await this.buildImageOnRemote(
|
|
6963
7239
|
spec.config,
|
|
6964
|
-
|
|
7240
|
+
remoteBuildDir,
|
|
6965
7241
|
dockerfileName,
|
|
6966
7242
|
tag,
|
|
6967
7243
|
spec.logger,
|
|
6968
|
-
signal
|
|
7244
|
+
{ signal, requiresBuildKit }
|
|
6969
7245
|
);
|
|
6970
7246
|
} else {
|
|
6971
7247
|
const { body, abortSignal, takeError } = packBuildContext(
|
|
6972
|
-
tree.contextDir,
|
|
7248
|
+
buildContextDir ?? tree.contextDir,
|
|
6973
7249
|
contextEntries ?? [],
|
|
6974
|
-
signal
|
|
7250
|
+
signal,
|
|
7251
|
+
ignoreContextPath
|
|
7252
|
+
);
|
|
7253
|
+
const builder = this.selectDockerodeBuilder(
|
|
7254
|
+
spec.config,
|
|
7255
|
+
requiresBuildKit,
|
|
7256
|
+
spec.logger
|
|
6975
7257
|
);
|
|
6976
7258
|
try {
|
|
6977
7259
|
const stream = await this.docker.buildImage(body, {
|
|
@@ -6984,9 +7266,10 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6984
7266
|
buildargs: { ...spec.config.envVars, NODE_ENV: "production" },
|
|
6985
7267
|
...dockerBuildResourceLimits(spec.config.resources),
|
|
6986
7268
|
forcerm: true,
|
|
7269
|
+
...builder.options,
|
|
6987
7270
|
abortSignal
|
|
6988
7271
|
});
|
|
6989
|
-
await this.streamDockerodeBuild(stream, spec.logger);
|
|
7272
|
+
await this.streamDockerodeBuild(stream, spec.logger, builder.trace);
|
|
6990
7273
|
} catch (err) {
|
|
6991
7274
|
const contextErr = takeError();
|
|
6992
7275
|
throw contextErr ? new Error(`Failed to read Docker build context: ${safeErrorMessage(contextErr)}`, { cause: contextErr }) : err;
|
|
@@ -7072,10 +7355,10 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7072
7355
|
}
|
|
7073
7356
|
const containers = await this.docker.listContainers({
|
|
7074
7357
|
all: true,
|
|
7075
|
-
filters: { label: [
|
|
7358
|
+
filters: { label: [OPENSHIP_LABEL.build] }
|
|
7076
7359
|
});
|
|
7077
7360
|
for (const c of containers) {
|
|
7078
|
-
const buildId = c.Labels?.[
|
|
7361
|
+
const buildId = c.Labels?.[OPENSHIP_LABEL.build];
|
|
7079
7362
|
if (!buildId || !cancelCovers(sessionId, buildId)) continue;
|
|
7080
7363
|
try {
|
|
7081
7364
|
await this.docker.getContainer(c.Id).remove({ force: true });
|
|
@@ -7096,10 +7379,18 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7096
7379
|
throw new Error("Docker deploy requires an imageRef (built image tag)");
|
|
7097
7380
|
}
|
|
7098
7381
|
const containerName = `openship-${config.runtimeName || config.projectId}-${config.deploymentId}`;
|
|
7382
|
+
const projectEnv = splitRuntimeEnv(config.envVars);
|
|
7383
|
+
if (projectEnv.dropped.length > 0) {
|
|
7384
|
+
log({
|
|
7385
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7386
|
+
level: "warn",
|
|
7387
|
+
message: droppedRuntimeEnvMessage(projectEnv.dropped)
|
|
7388
|
+
});
|
|
7389
|
+
}
|
|
7099
7390
|
const env = [
|
|
7100
7391
|
...config.portless ? [] : [`PORT=${config.port}`],
|
|
7101
7392
|
`NODE_ENV=${config.environment === "production" ? "production" : "development"}`,
|
|
7102
|
-
...
|
|
7393
|
+
...projectEnv.entries.map(([k, v]) => `${k}=${v}`)
|
|
7103
7394
|
];
|
|
7104
7395
|
const cmd = config.startCommand ? ["sh", "-c", config.startCommand] : void 0;
|
|
7105
7396
|
const restartPolicy = resolveRestartPolicy(config.restartPolicy);
|
|
@@ -7217,6 +7508,11 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7217
7508
|
await container.restart();
|
|
7218
7509
|
}
|
|
7219
7510
|
async removeImage(imageRef) {
|
|
7511
|
+
if (isArtifactPathRef(imageRef)) {
|
|
7512
|
+
throw new Error(
|
|
7513
|
+
`"${imageRef}" is a host directory, not an image reference \u2014 remove it with destroy().`
|
|
7514
|
+
);
|
|
7515
|
+
}
|
|
7220
7516
|
const image = this.docker.getImage(imageRef);
|
|
7221
7517
|
try {
|
|
7222
7518
|
await image.remove({ force: true });
|
|
@@ -7225,16 +7521,18 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7225
7521
|
}
|
|
7226
7522
|
}
|
|
7227
7523
|
async destroy(containerId) {
|
|
7228
|
-
if (containerId
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7524
|
+
if (isArtifactPathRef(containerId)) {
|
|
7525
|
+
if (this.transport.kind === "ssh") {
|
|
7526
|
+
const executor = this.connectionOptions?.executor;
|
|
7527
|
+
if (!executor) {
|
|
7528
|
+
throw new Error(
|
|
7529
|
+
`Cannot remove ${containerId}: the SSH transport has no command executor, and removing it locally would delete the wrong machine's files.`
|
|
7530
|
+
);
|
|
7531
|
+
}
|
|
7532
|
+
await removeManagedArtifact(executor, containerId);
|
|
7533
|
+
return;
|
|
7237
7534
|
}
|
|
7535
|
+
await removeManagedArtifact(null, containerId);
|
|
7238
7536
|
return;
|
|
7239
7537
|
}
|
|
7240
7538
|
const container = this.docker.getContainer(containerId);
|
|
@@ -7274,8 +7572,8 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7274
7572
|
return images.map((img) => ({
|
|
7275
7573
|
id: img.Id,
|
|
7276
7574
|
repoTags: (img.RepoTags ?? []).filter((t) => t && t !== "<none>:<none>"),
|
|
7277
|
-
buildId: img.Labels?.[
|
|
7278
|
-
deploymentId: img.Labels?.[
|
|
7575
|
+
buildId: img.Labels?.[OPENSHIP_LABEL.build],
|
|
7576
|
+
deploymentId: img.Labels?.[OPENSHIP_LABEL.deployment],
|
|
7279
7577
|
size: img.Size ?? 0
|
|
7280
7578
|
}));
|
|
7281
7579
|
}
|
|
@@ -7320,7 +7618,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7320
7618
|
return containers.map((c) => ({
|
|
7321
7619
|
containerId: c.Id,
|
|
7322
7620
|
status: stateMap[(c.State ?? "").toLowerCase().trim()] ?? "stopped",
|
|
7323
|
-
serviceName: c.Labels?.[
|
|
7621
|
+
serviceName: c.Labels?.[OPENSHIP_LABEL.service]
|
|
7324
7622
|
}));
|
|
7325
7623
|
}
|
|
7326
7624
|
// ── Rollback primitives (retention half only) ────────────────────────
|
|
@@ -7349,17 +7647,18 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7349
7647
|
}
|
|
7350
7648
|
}
|
|
7351
7649
|
async purge(deployment) {
|
|
7650
|
+
const failures = [];
|
|
7352
7651
|
if (deployment.containerId) {
|
|
7353
|
-
|
|
7354
|
-
await this.destroy(deployment.containerId);
|
|
7355
|
-
} catch {
|
|
7356
|
-
}
|
|
7652
|
+
await this.destroy(deployment.containerId).catch((err) => failures.push(err));
|
|
7357
7653
|
}
|
|
7358
7654
|
if (deployment.imageRef && ownsBuiltImage(deployment.imageRef)) {
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
|
|
7362
|
-
|
|
7655
|
+
await this.removeImage(deployment.imageRef).catch((err) => failures.push(err));
|
|
7656
|
+
}
|
|
7657
|
+
if (failures.length === 1) throw failures[0];
|
|
7658
|
+
if (failures.length > 1) {
|
|
7659
|
+
throw new Error(
|
|
7660
|
+
failures.map((err) => err instanceof Error ? err.message : String(err)).join("; ")
|
|
7661
|
+
);
|
|
7363
7662
|
}
|
|
7364
7663
|
}
|
|
7365
7664
|
/**
|
|
@@ -7677,7 +7976,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7677
7976
|
};
|
|
7678
7977
|
const startedAt = data.State.StartedAt;
|
|
7679
7978
|
const uptimeSeconds = startedAt && data.State.Running ? Math.floor((Date.now() - new Date(startedAt).getTime()) / 1e3) : void 0;
|
|
7680
|
-
const { ip, hostPort } = extractNetworkInfo(data);
|
|
7979
|
+
const { ip, hostPort, hostPortByContainerPort } = extractNetworkInfo(data);
|
|
7681
7980
|
let status;
|
|
7682
7981
|
if (data.State.Running) {
|
|
7683
7982
|
status = "running";
|
|
@@ -7692,6 +7991,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7692
7991
|
status,
|
|
7693
7992
|
ip,
|
|
7694
7993
|
hostPort,
|
|
7994
|
+
...hostPortByContainerPort ? { hostPortByContainerPort } : {},
|
|
7695
7995
|
uptimeSeconds: uptimeSeconds && uptimeSeconds > 0 ? uptimeSeconds : void 0
|
|
7696
7996
|
};
|
|
7697
7997
|
}
|
|
@@ -8177,7 +8477,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8177
8477
|
const ownNames = /* @__PURE__ */ new Set();
|
|
8178
8478
|
const foreign = /* @__PURE__ */ new Map();
|
|
8179
8479
|
for (const c of containers) {
|
|
8180
|
-
const owner = c.Labels?.[
|
|
8480
|
+
const owner = c.Labels?.[OPENSHIP_LABEL.project];
|
|
8181
8481
|
if (!owner) continue;
|
|
8182
8482
|
for (const m of c.Mounts ?? []) {
|
|
8183
8483
|
if (m.Type !== "volume" || !m.Name || !named.has(m.Name)) continue;
|
|
@@ -8231,7 +8531,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8231
8531
|
);
|
|
8232
8532
|
if (onNetwork) continue;
|
|
8233
8533
|
if (this.cannotJoinNetworks(c)) continue;
|
|
8234
|
-
const service = c.Labels?.[
|
|
8534
|
+
const service = c.Labels?.[OPENSHIP_LABEL.service];
|
|
8235
8535
|
try {
|
|
8236
8536
|
await network.connect({
|
|
8237
8537
|
Container: c.Id,
|
|
@@ -8255,7 +8555,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8255
8555
|
* doesn't exist (source not deployed) is skipped and nothing here ever throws —
|
|
8256
8556
|
* a link networking failure must never fail the consumer's deploy.
|
|
8257
8557
|
*/
|
|
8258
|
-
async attachToExternalNetworks(projectId, networkNames) {
|
|
8558
|
+
async attachToExternalNetworks(projectId, networkNames, extraContainerIds = []) {
|
|
8259
8559
|
if (networkNames.length === 0) return;
|
|
8260
8560
|
let containers;
|
|
8261
8561
|
try {
|
|
@@ -8266,6 +8566,22 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8266
8566
|
} catch {
|
|
8267
8567
|
return;
|
|
8268
8568
|
}
|
|
8569
|
+
if (extraContainerIds.length > 0) {
|
|
8570
|
+
const seen = new Set(containers.map((c) => c.Id));
|
|
8571
|
+
for (const id of extraContainerIds) {
|
|
8572
|
+
if (seen.has(id)) continue;
|
|
8573
|
+
try {
|
|
8574
|
+
const info = await this.docker.getContainer(id).inspect();
|
|
8575
|
+
containers.push({
|
|
8576
|
+
Id: info.Id,
|
|
8577
|
+
NetworkSettings: { Networks: info.NetworkSettings?.Networks ?? {} },
|
|
8578
|
+
HostConfig: { NetworkMode: info.HostConfig?.NetworkMode }
|
|
8579
|
+
});
|
|
8580
|
+
seen.add(info.Id);
|
|
8581
|
+
} catch {
|
|
8582
|
+
}
|
|
8583
|
+
}
|
|
8584
|
+
}
|
|
8269
8585
|
for (const name of networkNames) {
|
|
8270
8586
|
const network = this.docker.getNetwork(name);
|
|
8271
8587
|
let netId;
|
|
@@ -8319,9 +8635,17 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8319
8635
|
await existing.remove({ force: true });
|
|
8320
8636
|
} catch {
|
|
8321
8637
|
}
|
|
8638
|
+
const serviceEnv = splitRuntimeEnv(config.environment);
|
|
8639
|
+
if (serviceEnv.dropped.length > 0) {
|
|
8640
|
+
log({
|
|
8641
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8642
|
+
level: "warn",
|
|
8643
|
+
message: `[${config.serviceName}] ${droppedRuntimeEnvMessage(serviceEnv.dropped)}`
|
|
8644
|
+
});
|
|
8645
|
+
}
|
|
8322
8646
|
const env = [
|
|
8323
8647
|
...config.publicPort && config.environment.PORT === void 0 ? [`PORT=${config.publicPort}`] : [],
|
|
8324
|
-
...
|
|
8648
|
+
...serviceEnv.entries.map(([k, v]) => `${k}=${v}`)
|
|
8325
8649
|
];
|
|
8326
8650
|
const cmd = resolveComposeCmd(config);
|
|
8327
8651
|
const entrypoint = resolveComposeEntrypoint(config.advanced);
|
|
@@ -8405,7 +8729,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8405
8729
|
deploymentId: config.deploymentId,
|
|
8406
8730
|
projectId: config.projectId
|
|
8407
8731
|
}),
|
|
8408
|
-
|
|
8732
|
+
[OPENSHIP_LABEL.service]: config.serviceName
|
|
8409
8733
|
},
|
|
8410
8734
|
...healthcheck && { Healthcheck: healthcheck },
|
|
8411
8735
|
...stopConfig,
|
|
@@ -8444,7 +8768,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8444
8768
|
throw startErr;
|
|
8445
8769
|
}
|
|
8446
8770
|
const data = await container.inspect();
|
|
8447
|
-
const { ip, hostPort } = extractNetworkInfo(data);
|
|
8771
|
+
const { ip, hostPort, hostPortByContainerPort } = extractNetworkInfo(data);
|
|
8448
8772
|
const imageDigest = await this.resolveImageDigest(config.image).catch(() => void 0);
|
|
8449
8773
|
log({
|
|
8450
8774
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -8457,6 +8781,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8457
8781
|
status: "running",
|
|
8458
8782
|
ip,
|
|
8459
8783
|
hostPort,
|
|
8784
|
+
...hostPortByContainerPort ? { hostPortByContainerPort } : {},
|
|
8460
8785
|
imageDigest
|
|
8461
8786
|
};
|
|
8462
8787
|
}
|
|
@@ -8467,6 +8792,16 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8467
8792
|
* RepoDigests) or any inspect error. Works local + SSH (plain inspect, no
|
|
8468
8793
|
* followProgress). Never throws.
|
|
8469
8794
|
*/
|
|
8795
|
+
/**
|
|
8796
|
+
* PUBLIC because it is the only anchor the update scanner has.
|
|
8797
|
+
*
|
|
8798
|
+
* `resolveDeployedDrift` reads `service_deployment.image_digest` to tell a moved mutable
|
|
8799
|
+
* tag (`:latest`, `:16`) from an unchanged one — a REF comparison can't, since the ref
|
|
8800
|
+
* string is identical either way. A deploy records it here; an ADOPTED stack records
|
|
8801
|
+
* nothing, and an adopted stack is image-mode by definition (registry images, no repo),
|
|
8802
|
+
* so every migrated `postgres:16`/`redis:7` project was told "up to date" forever. The
|
|
8803
|
+
* adopt paths need to read it for a container they did not create.
|
|
8804
|
+
*/
|
|
8470
8805
|
async resolveImageDigest(ref) {
|
|
8471
8806
|
const info = await this.docker.getImage(ref).inspect();
|
|
8472
8807
|
const repoDigests = info?.RepoDigests ?? [];
|
|
@@ -8483,7 +8818,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8483
8818
|
};
|
|
8484
8819
|
|
|
8485
8820
|
export {
|
|
8486
|
-
|
|
8821
|
+
ensureScopedVolumeName,
|
|
8487
8822
|
isHostPathSource,
|
|
8488
8823
|
startExecStream,
|
|
8489
8824
|
startAttachStream,
|