openship 0.6.6 → 0.6.8
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-FA66N7GC.js +29 -0
- package/dist/{chunk-77QDF6UD.js → chunk-3MP6ACKM.js} +587 -156
- package/dist/{chunk-YIMOCZBZ.js → chunk-5DJOB7IX.js} +3 -3
- package/dist/{chunk-WVAML4H6.js → chunk-5I3COAFH.js} +274 -65
- package/dist/{chunk-5DMWSTOM.js → chunk-5UXHT6ED.js} +940 -224
- package/dist/{chunk-LUKYPVJ4.js → chunk-6GU6JSK6.js} +329 -466
- package/dist/{chunk-RSR7PFIQ.js → chunk-7PEXM2UI.js} +213 -45
- package/dist/{chunk-VTGPEC6B.js → chunk-7YBWXEM6.js} +6 -4
- package/dist/chunk-AHJYP7DG.js +938 -0
- package/dist/chunk-BO6CY4V2.js +265 -0
- package/dist/{chunk-FE7ZZQSE.js → chunk-D3OEMXYP.js} +3 -3
- package/dist/chunk-D6HU4VUZ.js +249 -0
- package/dist/{chunk-LFE5KQ7Y.js → chunk-ENKJ7XIF.js} +152 -57
- package/dist/{chunk-YMMQRE5J.js → chunk-GNOMEMNI.js} +1 -1
- package/dist/{chunk-QVW76WDL.js → chunk-IAMOUDHY.js} +190 -64
- package/dist/{chunk-OBPKYXQL.js → chunk-MUWVC5R4.js} +219 -58
- package/dist/chunk-N5YZMO33.js +111 -0
- package/dist/{chunk-A4GXYE4L.js → chunk-NIYD7MD2.js} +206 -52
- package/dist/{chunk-UVNMQM7W.js → chunk-NPSRGIHV.js} +8 -6
- package/dist/{chunk-2GWYYHOR.js → chunk-NRU2EBYZ.js} +2 -2
- package/dist/{chunk-CBLD2HS5.js → chunk-NUT6HKK6.js} +8 -5
- package/dist/{chunk-ZFJXPCQP.js → chunk-P7RCYELG.js} +4 -3
- package/dist/chunk-R4VDPFLS.js +106 -0
- package/dist/{chunk-HKRQXPTI.js → chunk-R5DC5PEF.js} +1 -1
- package/dist/{chunk-NK5N5LXN.js → chunk-RCMERVBL.js} +24 -9
- package/dist/{chunk-RXQBD4ZY.js → chunk-RHLNVZMS.js} +2 -2
- package/dist/chunk-SK4FFCFR.js +70 -0
- package/dist/{chunk-4I2BM2DT.js → chunk-T46BQOLY.js} +3 -3
- package/dist/{chunk-6FH4AXVY.js → chunk-T6DVUX4F.js} +1 -1
- package/dist/{chunk-PCPVTH6M.js → chunk-TAZZZKYO.js} +6 -4
- package/dist/{chunk-MLYSEZQB.js → chunk-UAWY6X7V.js} +43 -72
- package/dist/{chunk-Q7WGTQQ5.js → chunk-VAI5VOOI.js} +1 -1
- package/dist/{chunk-UJ6JZM65.js → chunk-WEZFTL4V.js} +1325 -513
- package/dist/{chunk-2YM3NI2E.js → chunk-X53JXRS4.js} +38 -44
- package/dist/{cloud-KRTZ2IL5.js → cloud-2CRDKOWQ.js} +1 -1
- package/dist/cloud-REV3DPJH.js +42 -0
- package/dist/{detect-OIE5V3VZ.js → detect-BBFT5GEU.js} +9 -6
- package/dist/{docker-VTR2GK5Y.js → docker-EG3QSE6I.js} +11 -8
- package/dist/{docker-edge-executor-22IBXNYJ.js → docker-edge-executor-E5TOTLMR.js} +8 -6
- package/dist/edge-import-YJVJUGYC.js +56 -0
- package/dist/{edge-preflight-GBNCPNVU.js → edge-preflight-QUW4BM7N.js} +15 -12
- package/dist/{ensure-container-edge-KPI2CVXJ.js → ensure-container-edge-FOEFQ3PI.js} +15 -10
- package/dist/{executor-7LXXZPBR.js → executor-NTWDJ3DB.js} +8 -6
- package/dist/host-channel-preflight-OMCZMMKR.js +54 -0
- package/dist/index.js +198 -76
- package/dist/{local-executor-WOFZD72X.js → local-executor-OQ2GRN7K.js} +3 -2
- package/dist/{nginx-KUFMLMAC.js → nginx-4U4C2XUS.js} +11 -8
- package/dist/{noop-NJ72F46T.js → noop-CLLJHCPL.js} +1 -1
- package/dist/{openresty-lua-ATTH35BE.js → openresty-lua-LLWT5XQW.js} +8 -5
- package/dist/{privilege-I4W3ZNZK.js → privilege-MNDVMDST.js} +6 -5
- package/dist/server/index.js +40049 -30378
- package/dist/server/migrations/0109_service_build_args.sql +9 -0
- package/dist/server/migrations/0110_service_deployment_host_ports.sql +18 -0
- package/dist/server/migrations/0111_host_port_claim.sql +381 -0
- package/dist/server/migrations/0112_backup_run_execution_lease.sql +12 -0
- package/dist/server/migrations/0113_docker_migration_execution_lease.sql +11 -0
- package/dist/server/migrations/0114_build_session_execution_lease_index.sql +6 -0
- package/dist/server/migrations/0115_orphan_target_key.sql +3 -0
- package/dist/server/migrations/0116_domain_ssl_challenge.sql +2 -0
- package/dist/server/migrations/meta/_journal.json +56 -0
- package/dist/setup-IFIMPQZJ.js +25 -0
- package/dist/{ssh-client-JBMA7SN4.js → ssh-client-2XWZCMGI.js} +3 -2
- package/dist/up-HQS7IR7H.js +61 -0
- package/package.json +1 -1
- package/dist/bare-OXJVW3AK.js +0 -25
- package/dist/chunk-LH2PLRQO.js +0 -434
- package/dist/chunk-SM26HDS2.js +0 -185
- 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,38 @@
|
|
|
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
|
-
resolveServiceDockerfile
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
resolveDockerBuildArgs,
|
|
11
|
+
resolveServiceDockerfile
|
|
12
|
+
} from "./chunk-6GU6JSK6.js";
|
|
13
|
+
import {
|
|
14
|
+
isArtifactPathRef,
|
|
15
|
+
removeManagedArtifact
|
|
16
|
+
} from "./chunk-SK4FFCFR.js";
|
|
13
17
|
import {
|
|
14
18
|
BuildCancelledError,
|
|
15
19
|
BuildLogger,
|
|
16
20
|
assembleGitClone,
|
|
21
|
+
builderContextRoot,
|
|
22
|
+
dockerBuildContextDirectory,
|
|
23
|
+
droppedRuntimeEnvMessage,
|
|
24
|
+
generateDockerfile,
|
|
25
|
+
isExcludedDocRootEntry,
|
|
17
26
|
killProcessesUnderDir,
|
|
18
27
|
materializeGitSsh,
|
|
28
|
+
normalizeDockerRelativePath,
|
|
19
29
|
parseLogLevel,
|
|
30
|
+
resolveContextDockerfileCandidates,
|
|
31
|
+
resolveDockerfileCandidates,
|
|
20
32
|
shellGitSshWriter,
|
|
33
|
+
splitRuntimeEnv,
|
|
21
34
|
sq,
|
|
35
|
+
staticBuilderOutputPath,
|
|
22
36
|
transferLocalDirectory
|
|
23
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-AHJYP7DG.js";
|
|
24
38
|
import {
|
|
25
39
|
DOCKER_DIAL_STDIO_COMMAND,
|
|
26
40
|
attachDialStdioDiagnostics,
|
|
@@ -29,23 +43,28 @@ import {
|
|
|
29
43
|
execSshCommand,
|
|
30
44
|
openDockerDialStdioChannel,
|
|
31
45
|
openSshUnixSocket
|
|
32
|
-
} from "./chunk-
|
|
46
|
+
} from "./chunk-TAZZZKYO.js";
|
|
33
47
|
import {
|
|
34
48
|
dirOf,
|
|
35
49
|
ensureOwnedDir
|
|
36
|
-
} from "./chunk-
|
|
50
|
+
} from "./chunk-NPSRGIHV.js";
|
|
51
|
+
import {
|
|
52
|
+
OPENSHIP_LABEL
|
|
53
|
+
} from "./chunk-D6HU4VUZ.js";
|
|
54
|
+
import {
|
|
55
|
+
isRuntimeNotFoundError
|
|
56
|
+
} from "./chunk-N5YZMO33.js";
|
|
37
57
|
import {
|
|
38
58
|
formatCpuCores,
|
|
39
59
|
formatMemoryMb,
|
|
40
60
|
hasCpuLimit,
|
|
41
61
|
hasMemoryLimit,
|
|
42
|
-
isRuntimeNotFoundError,
|
|
43
62
|
ownsNetworkEndpoint,
|
|
44
63
|
registryConfigKeys,
|
|
45
64
|
registryForImage,
|
|
46
65
|
safeErrorMessage,
|
|
47
66
|
withTimeout
|
|
48
|
-
} from "./chunk-
|
|
67
|
+
} from "./chunk-WEZFTL4V.js";
|
|
49
68
|
import {
|
|
50
69
|
__commonJS,
|
|
51
70
|
__require,
|
|
@@ -4609,6 +4628,7 @@ var tarFs = __toESM(require_tar_fs(), 1);
|
|
|
4609
4628
|
import Dockerode from "dockerode";
|
|
4610
4629
|
import { createGzip } from "zlib";
|
|
4611
4630
|
import { PassThrough, Writable } from "stream";
|
|
4631
|
+
import { relative, sep } from "path";
|
|
4612
4632
|
|
|
4613
4633
|
// ../../packages/adapters/src/runtime/docker-auth.ts
|
|
4614
4634
|
import { readFile } from "fs/promises";
|
|
@@ -4712,6 +4732,10 @@ var MODE_SUFFIX = /:(ro|rw|z|Z|nocopy)$/;
|
|
|
4712
4732
|
function scopedVolumeName(slug, name) {
|
|
4713
4733
|
return `openship-${slug}-${name}`;
|
|
4714
4734
|
}
|
|
4735
|
+
function ensureScopedVolumeName(slug, source) {
|
|
4736
|
+
const prefix = `openship-${slug}-`;
|
|
4737
|
+
return source.startsWith(prefix) ? source : scopedVolumeName(slug, source);
|
|
4738
|
+
}
|
|
4715
4739
|
function isHostPathSource(source) {
|
|
4716
4740
|
return source.startsWith("/") || source.startsWith("./") || source.startsWith("../") || source.startsWith("~");
|
|
4717
4741
|
}
|
|
@@ -4931,7 +4955,6 @@ import { join as join2 } from "path";
|
|
|
4931
4955
|
import net2 from "net";
|
|
4932
4956
|
var DEFAULT_REMOTE_DOCKER_SOCKET_PATH = "/var/run/docker.sock";
|
|
4933
4957
|
var resolvedDockerSocketPathCache = /* @__PURE__ */ new WeakMap();
|
|
4934
|
-
var STREAMLOCAL_PROBE_TIMEOUT_MS = 8e3;
|
|
4935
4958
|
var STREAMLOCAL_DATA_VERIFY_TIMEOUT_MS = 3e3;
|
|
4936
4959
|
var DIAL_STDIO_ANSWER_TIMEOUT_MS = 3e3;
|
|
4937
4960
|
var VERIFY_BUFFER_CAP_BYTES = 8 * 1024 * 1024;
|
|
@@ -5257,12 +5280,12 @@ function createDockerSshBridge(opts) {
|
|
|
5257
5280
|
try {
|
|
5258
5281
|
probe = await withTimeout(
|
|
5259
5282
|
openStreamlocalUpstream(opts),
|
|
5260
|
-
|
|
5261
|
-
`streamlocal open timed out after ${
|
|
5283
|
+
STREAMLOCAL_DATA_VERIFY_TIMEOUT_MS,
|
|
5284
|
+
`streamlocal open timed out after ${STREAMLOCAL_DATA_VERIFY_TIMEOUT_MS / 1e3}s`
|
|
5262
5285
|
);
|
|
5263
5286
|
const stream = probe;
|
|
5264
5287
|
const flowed = await new Promise((resolve) => {
|
|
5265
|
-
const timer = setTimeout(() => resolve(false),
|
|
5288
|
+
const timer = setTimeout(() => resolve(false), STREAMLOCAL_DATA_VERIFY_TIMEOUT_MS);
|
|
5266
5289
|
const settle = (ok) => {
|
|
5267
5290
|
clearTimeout(timer);
|
|
5268
5291
|
resolve(ok);
|
|
@@ -5512,7 +5535,7 @@ function resolveDockerTransport(opts) {
|
|
|
5512
5535
|
// ../../packages/adapters/src/runtime/docker.ts
|
|
5513
5536
|
var isDockerNotFoundError = isRuntimeNotFoundError;
|
|
5514
5537
|
function ownsBuiltImage(imageRef) {
|
|
5515
|
-
return
|
|
5538
|
+
return /^openship\/[a-z0-9][a-z0-9._-]*:bld_[A-Za-z0-9_-]+$/.test(imageRef);
|
|
5516
5539
|
}
|
|
5517
5540
|
function clampShellWindow(value, fallback, min, max) {
|
|
5518
5541
|
const n = Number.isFinite(value) ? Number(value) : fallback;
|
|
@@ -5580,14 +5603,7 @@ var IN_CONTAINER_EXEC_WATCHDOG = [
|
|
|
5580
5603
|
].join("\n");
|
|
5581
5604
|
function buildInContainerExecCmd(command, timeoutMs) {
|
|
5582
5605
|
const seconds = Math.max(1, Math.ceil(timeoutMs / 1e3));
|
|
5583
|
-
return [
|
|
5584
|
-
"sh",
|
|
5585
|
-
"-c",
|
|
5586
|
-
IN_CONTAINER_EXEC_WATCHDOG,
|
|
5587
|
-
"openship-exec",
|
|
5588
|
-
command,
|
|
5589
|
-
String(seconds)
|
|
5590
|
-
];
|
|
5606
|
+
return ["sh", "-c", IN_CONTAINER_EXEC_WATCHDOG, "openship-exec", command, String(seconds)];
|
|
5591
5607
|
}
|
|
5592
5608
|
async function resolveExecExitCode(exec, label) {
|
|
5593
5609
|
let info = await exec.inspect();
|
|
@@ -5701,9 +5717,10 @@ function parsePortBindings(portSpecs) {
|
|
|
5701
5717
|
}
|
|
5702
5718
|
return { exposedPorts, portBindings };
|
|
5703
5719
|
}
|
|
5704
|
-
function packBuildContext(contextDir, entries, cancelSignal) {
|
|
5720
|
+
function packBuildContext(contextDir, entries, cancelSignal, ignoreContextPath) {
|
|
5705
5721
|
const controller = new AbortController();
|
|
5706
|
-
const
|
|
5722
|
+
const ignore = ignoreContextPath ? (absolutePath) => ignoreContextPath(relative(contextDir, absolutePath).split(sep).join("/")) : void 0;
|
|
5723
|
+
const pack2 = tarFs.pack(contextDir, { entries, ignore });
|
|
5707
5724
|
const body = pack2.pipe(createGzip());
|
|
5708
5725
|
let contextError = null;
|
|
5709
5726
|
const capture = (err) => {
|
|
@@ -5721,7 +5738,7 @@ function packBuildContext(contextDir, entries, cancelSignal) {
|
|
|
5721
5738
|
var DURATION_UNITS_NS = {
|
|
5722
5739
|
ns: 1,
|
|
5723
5740
|
us: 1e3,
|
|
5724
|
-
|
|
5741
|
+
\u00B5s: 1e3,
|
|
5725
5742
|
ms: 1e6,
|
|
5726
5743
|
s: 1e9,
|
|
5727
5744
|
m: 6e10,
|
|
@@ -5829,20 +5846,45 @@ function firstNetworkIp(networks) {
|
|
|
5829
5846
|
}
|
|
5830
5847
|
function extractNetworkInfo(data) {
|
|
5831
5848
|
let ip;
|
|
5832
|
-
for (const net3 of Object.values(data.NetworkSettings
|
|
5849
|
+
for (const net3 of Object.values(data.NetworkSettings?.Networks ?? {})) {
|
|
5833
5850
|
if (net3.IPAddress) {
|
|
5834
5851
|
ip = net3.IPAddress;
|
|
5835
5852
|
break;
|
|
5836
5853
|
}
|
|
5837
5854
|
}
|
|
5855
|
+
const hostPortByContainerPort = {};
|
|
5838
5856
|
let hostPort;
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5857
|
+
const liveBindings = data.NetworkSettings?.Ports ?? {};
|
|
5858
|
+
const configuredBindings = data.HostConfig?.PortBindings ?? {};
|
|
5859
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(liveBindings), ...Object.keys(configuredBindings)])) {
|
|
5860
|
+
const [containerRaw, protocol = "tcp"] = key.split("/");
|
|
5861
|
+
if (protocol.toLowerCase() !== "tcp") continue;
|
|
5862
|
+
const containerPort = Number(containerRaw);
|
|
5863
|
+
if (!Number.isSafeInteger(containerPort) || containerPort < 1 || containerPort > 65535) {
|
|
5864
|
+
continue;
|
|
5865
|
+
}
|
|
5866
|
+
const live = liveBindings[key];
|
|
5867
|
+
const bindings = live && live.some((binding) => binding?.HostPort) ? live : configuredBindings[key];
|
|
5868
|
+
const candidates = (bindings ?? []).map((binding, index) => {
|
|
5869
|
+
const port = Number(binding?.HostPort);
|
|
5870
|
+
if (!Number.isSafeInteger(port) || port < 1 || port > 65535) return null;
|
|
5871
|
+
const ip2 = binding?.HostIp?.trim().replace(/^\[|\]$/g, "") ?? "";
|
|
5872
|
+
const priority = ip2 === "127.0.0.1" || ip2 === "::1" ? 3 : /^127(?:\.\d{1,3}){3}$/.test(ip2) ? 2 : ip2 && ip2 !== "0.0.0.0" && ip2 !== "::" ? 1 : 0;
|
|
5873
|
+
return { index, port, priority };
|
|
5874
|
+
}).filter(
|
|
5875
|
+
(candidate) => Boolean(candidate)
|
|
5876
|
+
).sort((left, right) => right.priority - left.priority || left.index - right.index);
|
|
5877
|
+
const selected = candidates[0]?.port;
|
|
5878
|
+
if (selected !== void 0) {
|
|
5879
|
+
hostPortByContainerPort[containerPort] = selected;
|
|
5880
|
+
hostPort ??= selected;
|
|
5843
5881
|
}
|
|
5844
5882
|
}
|
|
5845
|
-
return {
|
|
5883
|
+
return {
|
|
5884
|
+
ip,
|
|
5885
|
+
hostPort,
|
|
5886
|
+
...Object.keys(hostPortByContainerPort).length ? { hostPortByContainerPort } : {}
|
|
5887
|
+
};
|
|
5846
5888
|
}
|
|
5847
5889
|
function parseLoadedImageRef(output) {
|
|
5848
5890
|
const matches = [...(output || "").matchAll(/Loaded image(?: ID)?:\s*(\S+)/gi)];
|
|
@@ -5891,6 +5933,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
5891
5933
|
name = "docker";
|
|
5892
5934
|
capabilities = /* @__PURE__ */ new Set([
|
|
5893
5935
|
"build",
|
|
5936
|
+
"prebuiltImage",
|
|
5894
5937
|
"deploy",
|
|
5895
5938
|
"multiServiceDeploy",
|
|
5896
5939
|
"stop",
|
|
@@ -5999,11 +6042,11 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
5999
6042
|
/** Labels applied to both build images and deploy containers. */
|
|
6000
6043
|
labels(config) {
|
|
6001
6044
|
const l = {
|
|
6002
|
-
|
|
6045
|
+
[OPENSHIP_LABEL.project]: config.projectId
|
|
6003
6046
|
};
|
|
6004
|
-
if (config.deploymentId) l[
|
|
6005
|
-
if (config.sessionId) l[
|
|
6006
|
-
if (config.service) l[
|
|
6047
|
+
if (config.deploymentId) l[OPENSHIP_LABEL.deployment] = config.deploymentId;
|
|
6048
|
+
if (config.sessionId) l[OPENSHIP_LABEL.build] = config.sessionId;
|
|
6049
|
+
if (config.service) l[OPENSHIP_LABEL.service] = config.service;
|
|
6007
6050
|
return l;
|
|
6008
6051
|
}
|
|
6009
6052
|
// ── Build lifecycle ────────────────────────────────────────────────────
|
|
@@ -6045,12 +6088,20 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6045
6088
|
logger.log(`[${step}] ${label} ${message}`);
|
|
6046
6089
|
logger.step(step, status, message);
|
|
6047
6090
|
}
|
|
6048
|
-
handleBuildEvent(event, logger) {
|
|
6091
|
+
handleBuildEvent(event, logger, trace) {
|
|
6049
6092
|
const errorMessage = event.errorDetail?.message ?? event.error;
|
|
6050
6093
|
if (errorMessage) {
|
|
6051
6094
|
logger.log(errorMessage, "error");
|
|
6052
6095
|
return errorMessage;
|
|
6053
6096
|
}
|
|
6097
|
+
if (event.id === "moby.buildkit.trace" && typeof event.aux === "string") {
|
|
6098
|
+
let hint = null;
|
|
6099
|
+
for (const line of trace?.push(event.aux) ?? []) {
|
|
6100
|
+
logger.log(line.message, line.level === "error" ? "error" : parseLogLevel(line.message));
|
|
6101
|
+
hint ??= this.extractBuildFailureHint(line.message);
|
|
6102
|
+
}
|
|
6103
|
+
return hint;
|
|
6104
|
+
}
|
|
6054
6105
|
if (event.stream) {
|
|
6055
6106
|
const line = event.stream.trim();
|
|
6056
6107
|
if (!line) return null;
|
|
@@ -6107,6 +6158,12 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6107
6158
|
if (/returned a non-zero code:\s*\d+/i.test(line)) {
|
|
6108
6159
|
return line;
|
|
6109
6160
|
}
|
|
6161
|
+
if (/requires? BuildKit|BuildKit is enabled but the buildx component/i.test(line)) {
|
|
6162
|
+
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.`;
|
|
6163
|
+
}
|
|
6164
|
+
if (/buildkit (is )?(not (supported|enabled)|disabled)|builder version.*not supported/i.test(line)) {
|
|
6165
|
+
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.`;
|
|
6166
|
+
}
|
|
6110
6167
|
if (/\/workspace\/package\.json/i.test(line) && /ENOENT/i.test(line)) {
|
|
6111
6168
|
return "Docker build ran from /workspace but package.json was not found there. The configured rootDirectory is likely empty or incorrect.";
|
|
6112
6169
|
}
|
|
@@ -6158,26 +6215,69 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6158
6215
|
{ excludes: [] }
|
|
6159
6216
|
);
|
|
6160
6217
|
}
|
|
6218
|
+
/**
|
|
6219
|
+
* Is BuildKit usable on the remote host? Probed, never assumed: a plain `docker`
|
|
6220
|
+
* CLI without the buildx plugin HARD-FAILS when BuildKit is requested explicitly
|
|
6221
|
+
* ("BuildKit is enabled but the buildx component is missing or broken"), while an
|
|
6222
|
+
* unadorned `docker build` on the same box falls back to the legacy builder and
|
|
6223
|
+
* works — the same trap {@link buildImage} in system/managed-image.ts documents.
|
|
6224
|
+
* So we ask first, and only then opt in.
|
|
6225
|
+
*
|
|
6226
|
+
* Memoized per adapter instance (one instance ≈ one target daemon), so a compose
|
|
6227
|
+
* batch pays a single `docker buildx version` for N images.
|
|
6228
|
+
*/
|
|
6229
|
+
buildKitProbe = null;
|
|
6230
|
+
async remoteBuildKitAvailable() {
|
|
6231
|
+
const executor = this.connectionOptions?.executor;
|
|
6232
|
+
if (!executor) return false;
|
|
6233
|
+
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);
|
|
6234
|
+
const available = await probe;
|
|
6235
|
+
if (!available) this.buildKitProbe = null;
|
|
6236
|
+
return available;
|
|
6237
|
+
}
|
|
6161
6238
|
/**
|
|
6162
6239
|
* Run native `docker build` on the remote host against an already-transferred
|
|
6163
|
-
* context dir. One image; `dockerfileName` selects which Dockerfile
|
|
6164
|
-
*
|
|
6240
|
+
* context dir. One image; `dockerfileName` selects which Dockerfile to use (so N
|
|
6241
|
+
* services can build from one transferred tree) and is relative to
|
|
6242
|
+
* `remoteBuildDir` — the build CONTEXT, which for a compose service is its own
|
|
6243
|
+
* `build:` directory inside that tree, not the tree root (#634).
|
|
6165
6244
|
*/
|
|
6166
|
-
async buildImageOnRemote(config,
|
|
6245
|
+
async buildImageOnRemote(config, remoteBuildDir, dockerfileName, tag, log, opts) {
|
|
6246
|
+
const signal = opts?.signal;
|
|
6167
6247
|
const executor = this.connectionOptions?.executor;
|
|
6168
6248
|
if (!executor) throw new Error("SSH build path requires an executor on connectionOptions");
|
|
6169
|
-
const
|
|
6170
|
-
|
|
6171
|
-
NODE_ENV: "production"
|
|
6172
|
-
}).filter(([k]) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(k)).map(([k, v]) => `--build-arg ${sq(`${k}=${v}`)}`).join(" ");
|
|
6249
|
+
const resolvedBuildArgs = resolveDockerBuildArgs(config);
|
|
6250
|
+
const buildArgs = Object.entries(resolvedBuildArgs).map(([k, v]) => `--build-arg ${sq(`${k}=${v}`)}`).join(" ");
|
|
6173
6251
|
const labelArgs = Object.entries(
|
|
6174
6252
|
this.labels({ projectId: config.projectId, sessionId: config.sessionId })
|
|
6175
6253
|
).map(([k, v]) => `--label ${sq(`${k}=${v}`)}`).join(" ");
|
|
6176
6254
|
const dockerfileFlag = dockerfileName && dockerfileName !== "Dockerfile" ? ` -f ${sq(dockerfileName)}` : "";
|
|
6177
|
-
const
|
|
6178
|
-
|
|
6255
|
+
const buildKit = await this.remoteBuildKitAvailable();
|
|
6256
|
+
const builderEnv = buildKit ? "DOCKER_BUILDKIT=1 " : "";
|
|
6257
|
+
const builderFlags = buildKit ? " --progress=plain" : " --force-rm";
|
|
6258
|
+
if (buildKit) {
|
|
6259
|
+
log.log("Builder: BuildKit (docker buildx detected on the host)");
|
|
6260
|
+
} else if (opts?.requiresBuildKit) {
|
|
6261
|
+
log.log(
|
|
6262
|
+
"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.",
|
|
6263
|
+
"warn"
|
|
6264
|
+
);
|
|
6265
|
+
} else {
|
|
6266
|
+
log.log(
|
|
6267
|
+
"Builder: legacy (docker buildx not available on the host) \u2014 BuildKit-only Dockerfile syntax such as `RUN --mount` will not work"
|
|
6268
|
+
);
|
|
6269
|
+
}
|
|
6270
|
+
const buildCmd = `cd ${sq(remoteBuildDir)} && ${builderEnv}docker build${builderFlags} -t ${sq(tag)}${dockerfileFlag} ${labelArgs} ${buildArgs} .`;
|
|
6271
|
+
log.log(
|
|
6272
|
+
`Running Docker build on remote (${Object.keys(resolvedBuildArgs).length} build argument${Object.keys(resolvedBuildArgs).length === 1 ? "" : "s"}; values hidden).`
|
|
6273
|
+
);
|
|
6179
6274
|
log.log("\u2500\u2500\u2500 docker build output \u2500\u2500\u2500");
|
|
6180
|
-
this.emitDockerStep(
|
|
6275
|
+
this.emitDockerStep(
|
|
6276
|
+
log,
|
|
6277
|
+
"install",
|
|
6278
|
+
"running",
|
|
6279
|
+
"Running install inside container (docker build)"
|
|
6280
|
+
);
|
|
6181
6281
|
const { code } = await executor.streamExec(
|
|
6182
6282
|
buildCmd,
|
|
6183
6283
|
(entry) => {
|
|
@@ -6238,7 +6338,11 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6238
6338
|
config.gitSsh
|
|
6239
6339
|
);
|
|
6240
6340
|
}
|
|
6241
|
-
const {
|
|
6341
|
+
const {
|
|
6342
|
+
cloneUrl,
|
|
6343
|
+
gitEnv: GIT_ENV,
|
|
6344
|
+
credFlag: CRED
|
|
6345
|
+
} = assembleGitClone({
|
|
6242
6346
|
repoUrl: config.repoUrl,
|
|
6243
6347
|
gitToken: config.gitToken,
|
|
6244
6348
|
gitCredentialHelperPath: config.gitCredentialHelperPath,
|
|
@@ -6264,8 +6368,11 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6264
6368
|
`${GIT_ENV} git ${CRED} clone --progress --depth 50 --branch ${sq(config.branch)} ${sq(cloneUrl)} ${dir} && cd ${dir} && git ${CRED} -c advice.detachedHead=false checkout ${sq(config.commitSha)}`
|
|
6265
6369
|
);
|
|
6266
6370
|
} catch {
|
|
6267
|
-
log.log(
|
|
6268
|
-
|
|
6371
|
+
log.log(
|
|
6372
|
+
`Commit ${config.commitSha} not in the shallow clone; unshallowing and retrying.
|
|
6373
|
+
`,
|
|
6374
|
+
"warn"
|
|
6375
|
+
);
|
|
6269
6376
|
await run(
|
|
6270
6377
|
`cd ${dir} && ${GIT_ENV} git ${CRED} fetch --progress --unshallow && git ${CRED} -c advice.detachedHead=false checkout ${sq(config.commitSha)}`
|
|
6271
6378
|
);
|
|
@@ -6286,15 +6393,50 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6286
6393
|
* (clone-on-server). Mirrors resolveServiceDockerfile but probes the remote
|
|
6287
6394
|
* tree with `test -f` instead of the local FS: a repository Dockerfile
|
|
6288
6395
|
* candidate is used when present; otherwise a Dockerfile is generated locally
|
|
6289
|
-
* (pure fn) and written to the remote tree.
|
|
6290
|
-
*
|
|
6396
|
+
* (pure fn) and written to the remote tree.
|
|
6397
|
+
*
|
|
6398
|
+
* Returns the build CONTEXT dir on the host plus the Dockerfile path relative to
|
|
6399
|
+
* it — the same pair {@link resolveServiceDockerfile} produces locally, because
|
|
6400
|
+
* these two resolvers are one behaviour with two probes and a narrowed context
|
|
6401
|
+
* learned by only one of them would leave #634 reproducible on this target.
|
|
6291
6402
|
*/
|
|
6292
|
-
async resolveRemoteDockerfile(config, remoteContextDir, generatedName, requireRepositoryDockerfile) {
|
|
6403
|
+
async resolveRemoteDockerfile(config, remoteContextDir, generatedName, requireRepositoryDockerfile, log) {
|
|
6293
6404
|
const executor = this.connectionOptions?.executor;
|
|
6294
6405
|
if (!executor) throw new Error("Clone-on-server requires an SSH executor on connectionOptions");
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6406
|
+
const contextSubdir = dockerBuildContextDirectory(config);
|
|
6407
|
+
const remoteBuildDir = contextSubdir ? `${remoteContextDir}/${contextSubdir}` : remoteContextDir;
|
|
6408
|
+
if (contextSubdir) {
|
|
6409
|
+
const real = await executor.exec(`cd ${sq(remoteBuildDir)} 2>/dev/null && pwd -P || true`).then((out) => out.trim()).catch(() => "");
|
|
6410
|
+
const root = await executor.exec(`cd ${sq(remoteContextDir)} && pwd -P`).then((out) => out.trim()).catch(() => "");
|
|
6411
|
+
if (!real) {
|
|
6412
|
+
throw new Error(
|
|
6413
|
+
`Build context "${contextSubdir}" is not a directory in the cloned source. Check the service's build path.`
|
|
6414
|
+
);
|
|
6415
|
+
}
|
|
6416
|
+
if (!root || real !== root && !real.startsWith(`${root}/`)) {
|
|
6417
|
+
throw new Error(`Build context "${contextSubdir}" resolves outside the cloned source.`);
|
|
6418
|
+
}
|
|
6419
|
+
}
|
|
6420
|
+
const candidates = contextSubdir ? resolveContextDockerfileCandidates(contextSubdir, config.dockerfilePath) : resolveDockerfileCandidates(config.rootDirectory, config.dockerfilePath);
|
|
6421
|
+
for (const candidate of candidates) {
|
|
6422
|
+
const out = await executor.exec(`test -f ${sq(`${remoteBuildDir}/${candidate}`)} && echo yes || true`).catch(() => "");
|
|
6423
|
+
if (out.trim() === "yes") {
|
|
6424
|
+
const explicit = normalizeDockerRelativePath(config.dockerfilePath);
|
|
6425
|
+
if (contextSubdir && explicit && candidate === "Dockerfile" && explicit !== "Dockerfile" && explicit !== `${contextSubdir}/Dockerfile`) {
|
|
6426
|
+
log?.log(
|
|
6427
|
+
`Configured Dockerfile "${config.dockerfilePath}" was not found in build context "${contextSubdir}" \u2014 using ${contextSubdir}/Dockerfile instead.`,
|
|
6428
|
+
"warn"
|
|
6429
|
+
);
|
|
6430
|
+
}
|
|
6431
|
+
return { remoteBuildDir, dockerfileName: candidate };
|
|
6432
|
+
}
|
|
6433
|
+
}
|
|
6434
|
+
if (contextSubdir) {
|
|
6435
|
+
throw new Error(
|
|
6436
|
+
missingContextDockerfileMessage(contextSubdir, config.dockerfilePath, {
|
|
6437
|
+
generatedRecipeRefused: !requireRepositoryDockerfile
|
|
6438
|
+
})
|
|
6439
|
+
);
|
|
6298
6440
|
}
|
|
6299
6441
|
if (requireRepositoryDockerfile) {
|
|
6300
6442
|
const expected = config.dockerfilePath?.trim() || "Dockerfile";
|
|
@@ -6303,7 +6445,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6303
6445
|
);
|
|
6304
6446
|
}
|
|
6305
6447
|
await executor.writeFile(`${remoteContextDir}/${generatedName}`, generateDockerfile(config));
|
|
6306
|
-
return generatedName;
|
|
6448
|
+
return { remoteBuildDir, dockerfileName: generatedName };
|
|
6307
6449
|
}
|
|
6308
6450
|
async buildViaSshTarPipe(config, buildContext, tag, log, signal) {
|
|
6309
6451
|
const remoteContextDir = `/tmp/openship-build-${config.sessionId}`;
|
|
@@ -6311,11 +6453,11 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6311
6453
|
await this.transferBuildContext(buildContext.contextDir, remoteContextDir, log);
|
|
6312
6454
|
await this.buildImageOnRemote(
|
|
6313
6455
|
config,
|
|
6314
|
-
remoteContextDir,
|
|
6456
|
+
buildContext.contextSubdir ? `${remoteContextDir}/${buildContext.contextSubdir}` : remoteContextDir,
|
|
6315
6457
|
buildContext.dockerfileName,
|
|
6316
6458
|
tag,
|
|
6317
6459
|
log,
|
|
6318
|
-
signal
|
|
6460
|
+
{ signal, requiresBuildKit: buildContext.requiresBuildKit }
|
|
6319
6461
|
);
|
|
6320
6462
|
} finally {
|
|
6321
6463
|
this.connectionOptions?.executor?.exec(`rm -rf ${sq(remoteContextDir)}`).catch(() => {
|
|
@@ -6323,6 +6465,41 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6323
6465
|
await buildContext.cleanup();
|
|
6324
6466
|
}
|
|
6325
6467
|
}
|
|
6468
|
+
/**
|
|
6469
|
+
* Pick the Engine-API builder for ONE dockerode build, for BOTH dockerode call
|
|
6470
|
+
* sites (single image and compose batch) — they are the same decision and a
|
|
6471
|
+
* builder learned by only one of them is the "two executors" trap all over again.
|
|
6472
|
+
*
|
|
6473
|
+
* The classic builder is kept as the DEFAULT deliberately. It is what every
|
|
6474
|
+
* self-hosted local build has always used; switching wholesale would change layer
|
|
6475
|
+
* caching for everyone and silently drop the build CPU/memory caps, which BuildKit
|
|
6476
|
+
* ignores. So BuildKit is opted into only where the classic builder cannot work at
|
|
6477
|
+
* all — a Dockerfile whose syntax it rejects (#634).
|
|
6478
|
+
*
|
|
6479
|
+
* Through the ENGINE API (`version=2`), not by shelling out to `docker build`,
|
|
6480
|
+
* which is the first alternative that comes to mind and is not available: the API
|
|
6481
|
+
* image ships `openssh-client rsync git` and no docker CLI (apps/api/Dockerfile),
|
|
6482
|
+
* and the transport may be a TCP socket with no shell at the other end at all.
|
|
6483
|
+
* Nothing needs the buildx plugin either — BuildKit lives in the daemon.
|
|
6484
|
+
*
|
|
6485
|
+
* Asymmetric with the remote CLI path on purpose: there, BuildKit is taken whenever
|
|
6486
|
+
* buildx exists, because that command passes no --memory/--cpus (so there is no cap
|
|
6487
|
+
* to lose) and on docker ≥23 the CLI already defaults to it — the prefix mostly
|
|
6488
|
+
* buys the log line and the buildx-missing warning.
|
|
6489
|
+
*/
|
|
6490
|
+
selectDockerodeBuilder(config, requiresBuildKit, log) {
|
|
6491
|
+
if (!requiresBuildKit) return { options: {} };
|
|
6492
|
+
log.log(
|
|
6493
|
+
"Dockerfile uses BuildKit-only syntax (RUN --mount, a syntax directive or a heredoc) \u2014 building with BuildKit."
|
|
6494
|
+
);
|
|
6495
|
+
if (Object.keys(dockerBuildResourceLimits(config.resources)).length > 0) {
|
|
6496
|
+
log.log(
|
|
6497
|
+
"Build CPU/memory limits are not enforced under BuildKit \u2014 this build runs uncapped.",
|
|
6498
|
+
"warn"
|
|
6499
|
+
);
|
|
6500
|
+
}
|
|
6501
|
+
return { options: { version: "2" }, trace: new BuildKitTraceDecoder() };
|
|
6502
|
+
}
|
|
6326
6503
|
/**
|
|
6327
6504
|
* Dockerode build path. Used for local socket and TCP transports, plus
|
|
6328
6505
|
* SSH transports that didn't get an executor wired in (shouldn't happen
|
|
@@ -6335,32 +6512,34 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6335
6512
|
async buildViaDockerode(config, buildContext, tag, log, cancelSignal) {
|
|
6336
6513
|
log.log(`Streaming build context to Docker daemon - image tag: ${tag}`);
|
|
6337
6514
|
const { body, abortSignal, takeError } = packBuildContext(
|
|
6338
|
-
buildContext.
|
|
6515
|
+
buildContext.buildContextDir,
|
|
6339
6516
|
buildContext.contextEntries,
|
|
6340
|
-
cancelSignal
|
|
6517
|
+
cancelSignal,
|
|
6518
|
+
buildContext.ignoreContextPath
|
|
6341
6519
|
);
|
|
6520
|
+
const builder = this.selectDockerodeBuilder(config, buildContext.requiresBuildKit, log);
|
|
6342
6521
|
try {
|
|
6343
6522
|
const stream = await this.docker.buildImage(body, {
|
|
6344
6523
|
t: tag,
|
|
6345
6524
|
dockerfile: buildContext.dockerfileName,
|
|
6346
6525
|
labels: this.labels({ projectId: config.projectId, sessionId: config.sessionId }),
|
|
6347
|
-
buildargs:
|
|
6348
|
-
...config.envVars,
|
|
6349
|
-
NODE_ENV: "production"
|
|
6350
|
-
},
|
|
6526
|
+
buildargs: resolveDockerBuildArgs(config),
|
|
6351
6527
|
// Omitted entirely unless the project set a build cap — a self-hosted
|
|
6352
6528
|
// build should be free to use the machine (a production build often
|
|
6353
6529
|
// needs several GB). Opt-in only; see dockerBuildResourceLimits.
|
|
6354
6530
|
...dockerBuildResourceLimits(config.resources),
|
|
6355
6531
|
forcerm: true,
|
|
6532
|
+
...builder.options,
|
|
6356
6533
|
abortSignal
|
|
6357
6534
|
});
|
|
6358
6535
|
log.log("Connected to Docker daemon. Build output follows:");
|
|
6359
|
-
await this.streamDockerodeBuild(stream, log);
|
|
6536
|
+
await this.streamDockerodeBuild(stream, log, builder.trace);
|
|
6360
6537
|
log.log("Docker daemon finished streaming build output. Finalizing image...\n");
|
|
6361
6538
|
} catch (err) {
|
|
6362
6539
|
const contextErr = takeError();
|
|
6363
|
-
throw contextErr ? new Error(`Failed to read Docker build context: ${safeErrorMessage(contextErr)}`, {
|
|
6540
|
+
throw contextErr ? new Error(`Failed to read Docker build context: ${safeErrorMessage(contextErr)}`, {
|
|
6541
|
+
cause: contextErr
|
|
6542
|
+
}) : err;
|
|
6364
6543
|
} finally {
|
|
6365
6544
|
await buildContext.cleanup();
|
|
6366
6545
|
}
|
|
@@ -6370,7 +6549,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6370
6549
|
* idle timeout + keepalive, and throw on a fatal build event. Shared by the
|
|
6371
6550
|
* single-image dockerode path and the local batch build (buildImages).
|
|
6372
6551
|
*/
|
|
6373
|
-
async streamDockerodeBuild(stream, log) {
|
|
6552
|
+
async streamDockerodeBuild(stream, log, trace) {
|
|
6374
6553
|
let fatalBuildError = null;
|
|
6375
6554
|
await new Promise((resolve, reject) => {
|
|
6376
6555
|
let settled = false;
|
|
@@ -6409,9 +6588,11 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6409
6588
|
}, 6e4);
|
|
6410
6589
|
if (keepaliveTimer.unref) keepaliveTimer.unref();
|
|
6411
6590
|
idleTimer = setTimeout(() => {
|
|
6412
|
-
fail(
|
|
6413
|
-
|
|
6414
|
-
|
|
6591
|
+
fail(
|
|
6592
|
+
new Error(
|
|
6593
|
+
"Docker build produced no output for 30 minutes. This usually means the remote server cannot reach the package registry, has broken DNS, or the Docker daemon stalled during the build."
|
|
6594
|
+
)
|
|
6595
|
+
);
|
|
6415
6596
|
}, DOCKER_BUILD_IDLE_TIMEOUT_MS);
|
|
6416
6597
|
if (idleTimer.unref) idleTimer.unref();
|
|
6417
6598
|
};
|
|
@@ -6427,7 +6608,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6427
6608
|
},
|
|
6428
6609
|
(event) => {
|
|
6429
6610
|
resetIdleTimer();
|
|
6430
|
-
fatalBuildError ??= this.handleBuildEvent(event, log);
|
|
6611
|
+
fatalBuildError ??= this.handleBuildEvent(event, log, trace);
|
|
6431
6612
|
}
|
|
6432
6613
|
);
|
|
6433
6614
|
});
|
|
@@ -6496,20 +6677,16 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6496
6677
|
await this.cloneSourceOnRemote(config, remoteContextDir, log);
|
|
6497
6678
|
if (abort.signal.aborted) throw new BuildCancelledError();
|
|
6498
6679
|
this.emitDockerStep(log, "clone", "completed", "Source cloned on the server");
|
|
6499
|
-
const dockerfileName = await this.resolveRemoteDockerfile(
|
|
6680
|
+
const { remoteBuildDir, dockerfileName } = await this.resolveRemoteDockerfile(
|
|
6500
6681
|
config,
|
|
6501
6682
|
remoteContextDir,
|
|
6502
6683
|
"Dockerfile.openship",
|
|
6503
|
-
config.stack === "docker"
|
|
6504
|
-
|
|
6505
|
-
await this.buildImageOnRemote(
|
|
6506
|
-
config,
|
|
6507
|
-
remoteContextDir,
|
|
6508
|
-
dockerfileName,
|
|
6509
|
-
tag,
|
|
6510
|
-
log,
|
|
6511
|
-
abort.signal
|
|
6684
|
+
config.stack === "docker",
|
|
6685
|
+
log
|
|
6512
6686
|
);
|
|
6687
|
+
await this.buildImageOnRemote(config, remoteBuildDir, dockerfileName, tag, log, {
|
|
6688
|
+
signal: abort.signal
|
|
6689
|
+
});
|
|
6513
6690
|
} finally {
|
|
6514
6691
|
sshExecutor.exec(`rm -rf ${sq(remoteContextDir)}`).catch(() => {
|
|
6515
6692
|
});
|
|
@@ -6519,7 +6696,12 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6519
6696
|
log.log(`Image ${tag} is ready.
|
|
6520
6697
|
`);
|
|
6521
6698
|
log.step("build", "completed", `Finalizing image ${tag}`);
|
|
6522
|
-
return {
|
|
6699
|
+
return {
|
|
6700
|
+
sessionId: config.sessionId,
|
|
6701
|
+
status: "deploying",
|
|
6702
|
+
imageRef: tag,
|
|
6703
|
+
durationMs: Date.now() - startTime
|
|
6704
|
+
};
|
|
6523
6705
|
}
|
|
6524
6706
|
this.emitDockerStep(log, "clone", "running", "Preparing Docker build context...");
|
|
6525
6707
|
const buildContext = await createDockerBuildContext(config, {
|
|
@@ -6530,16 +6712,13 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6530
6712
|
try {
|
|
6531
6713
|
const sizeBytes = await this.estimateContextSize(buildContext.contextDir);
|
|
6532
6714
|
const sizeMB = (sizeBytes / 1024 / 1024).toFixed(1);
|
|
6533
|
-
this.emitDockerStep(
|
|
6534
|
-
log,
|
|
6535
|
-
"clone",
|
|
6536
|
-
"completed",
|
|
6537
|
-
`Docker build context ready (${sizeMB} MB)`
|
|
6538
|
-
);
|
|
6715
|
+
this.emitDockerStep(log, "clone", "completed", `Docker build context ready (${sizeMB} MB)`);
|
|
6539
6716
|
} catch {
|
|
6540
6717
|
this.emitDockerStep(log, "clone", "completed", "Docker build context ready");
|
|
6541
6718
|
}
|
|
6542
|
-
if (buildContext.
|
|
6719
|
+
if (buildContext.contextSubdir) {
|
|
6720
|
+
log.log(`Using Docker build context: ${buildContext.contextSubdir}`);
|
|
6721
|
+
} else if (buildContext.rootDirectory) {
|
|
6543
6722
|
log.log(`Using Docker build root: ${buildContext.rootDirectory}`);
|
|
6544
6723
|
}
|
|
6545
6724
|
if (buildContext.usesRepositoryDockerfile) {
|
|
@@ -6579,7 +6758,75 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6579
6758
|
if (abort.signal.aborted || err instanceof BuildCancelledError) return cancelled();
|
|
6580
6759
|
const msg = safeErrorMessage(err);
|
|
6581
6760
|
log.step("build", "failed", `Docker build failed: ${msg}`);
|
|
6582
|
-
return {
|
|
6761
|
+
return {
|
|
6762
|
+
sessionId: config.sessionId,
|
|
6763
|
+
status: "failed",
|
|
6764
|
+
durationMs: Date.now() - startTime,
|
|
6765
|
+
errorMessage: `Docker build failed: ${msg}`
|
|
6766
|
+
};
|
|
6767
|
+
} finally {
|
|
6768
|
+
releaseDockerBuild(config.sessionId, abort);
|
|
6769
|
+
}
|
|
6770
|
+
}
|
|
6771
|
+
/**
|
|
6772
|
+
* Acquire a registry image without ever treating it as a Dockerfile builder.
|
|
6773
|
+
*
|
|
6774
|
+
* The returned digest is preferred over the requested tag so a later deploy
|
|
6775
|
+
* runs exactly the bytes acquired here even if a mutable tag moves between
|
|
6776
|
+
* prepare and deploy. The image is deliberately non-owned: it came from a
|
|
6777
|
+
* registry and may be shared by unrelated containers on the target daemon, so
|
|
6778
|
+
* deployment cleanup must never remove it as if Openship had built it.
|
|
6779
|
+
*/
|
|
6780
|
+
async prepareImage(config, logger) {
|
|
6781
|
+
const log = logger ?? new BuildLogger();
|
|
6782
|
+
const startTime = Date.now();
|
|
6783
|
+
const abort = registerDockerBuild(config.sessionId);
|
|
6784
|
+
const cancelled = () => {
|
|
6785
|
+
log.step("build", "failed", "Image pull cancelled");
|
|
6786
|
+
return {
|
|
6787
|
+
sessionId: config.sessionId,
|
|
6788
|
+
status: "cancelled",
|
|
6789
|
+
durationMs: Date.now() - startTime,
|
|
6790
|
+
artifactOwned: false
|
|
6791
|
+
};
|
|
6792
|
+
};
|
|
6793
|
+
try {
|
|
6794
|
+
if (abort.signal.aborted) return cancelled();
|
|
6795
|
+
const requestedRef = config.imageRef.trim();
|
|
6796
|
+
if (!requestedRef) throw new Error("Prebuilt image reference is required");
|
|
6797
|
+
log.step("build", "running", `Pulling prebuilt image ${requestedRef}`);
|
|
6798
|
+
try {
|
|
6799
|
+
await this.ensureDockerFeature(log);
|
|
6800
|
+
} catch (featureErr) {
|
|
6801
|
+
throw new Error(this.formatDockerConnectivityError(featureErr));
|
|
6802
|
+
}
|
|
6803
|
+
await this.pullImage(requestedRef, { force: config.forcePull });
|
|
6804
|
+
if (abort.signal.aborted) return cancelled();
|
|
6805
|
+
const deployRef = await this.resolveImageDigest(requestedRef).catch(() => void 0) ?? requestedRef;
|
|
6806
|
+
log.log(
|
|
6807
|
+
deployRef === requestedRef ? `Image ${requestedRef} is ready.
|
|
6808
|
+
` : `Image ${requestedRef} is ready as ${deployRef}.
|
|
6809
|
+
`
|
|
6810
|
+
);
|
|
6811
|
+
log.step("build", "completed", `Prebuilt image ${deployRef} is ready`);
|
|
6812
|
+
return {
|
|
6813
|
+
sessionId: config.sessionId,
|
|
6814
|
+
status: "deploying",
|
|
6815
|
+
imageRef: deployRef,
|
|
6816
|
+
durationMs: Date.now() - startTime,
|
|
6817
|
+
artifactOwned: false
|
|
6818
|
+
};
|
|
6819
|
+
} catch (err) {
|
|
6820
|
+
if (abort.signal.aborted) return cancelled();
|
|
6821
|
+
const message = safeErrorMessage(err);
|
|
6822
|
+
log.step("build", "failed", `Image pull failed: ${message}`);
|
|
6823
|
+
return {
|
|
6824
|
+
sessionId: config.sessionId,
|
|
6825
|
+
status: "failed",
|
|
6826
|
+
durationMs: Date.now() - startTime,
|
|
6827
|
+
errorMessage: `Image pull failed: ${message}`,
|
|
6828
|
+
artifactOwned: false
|
|
6829
|
+
};
|
|
6583
6830
|
} finally {
|
|
6584
6831
|
releaseDockerBuild(config.sessionId, abort);
|
|
6585
6832
|
}
|
|
@@ -6608,6 +6855,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6608
6855
|
await this.extractDocRootViaDaemon(tag, docRoot, hostOutDir);
|
|
6609
6856
|
}
|
|
6610
6857
|
}
|
|
6858
|
+
await this.pruneBuildFilesFromDocRoot(config, hostOutDir, sshExecutor ?? null, log);
|
|
6611
6859
|
log.log(`Static files ready at ${hostOutDir}
|
|
6612
6860
|
`);
|
|
6613
6861
|
} finally {
|
|
@@ -6692,6 +6940,74 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6692
6940
|
* expensive one: every extractor can succeed having copied nothing, which deploys
|
|
6693
6941
|
* green and then 404s the entire site.
|
|
6694
6942
|
*/
|
|
6943
|
+
/**
|
|
6944
|
+
* Names that must never end up in a public doc-root, matched at its TOP LEVEL
|
|
6945
|
+
* only.
|
|
6946
|
+
*
|
|
6947
|
+
* `.git` is the repository — serving it publishes full source history. The
|
|
6948
|
+
* Dockerfiles and `.dockerignore` are OUR build inputs, and one of them is
|
|
6949
|
+
* literally written into the tree by `resolveServiceDockerfile`.
|
|
6950
|
+
*/
|
|
6951
|
+
static DOC_ROOT_EXCLUDED = [
|
|
6952
|
+
".dockerignore",
|
|
6953
|
+
".git",
|
|
6954
|
+
"Dockerfile",
|
|
6955
|
+
"Dockerfile.*"
|
|
6956
|
+
];
|
|
6957
|
+
/**
|
|
6958
|
+
* Remove build inputs from an extracted static doc-root, then re-assert the
|
|
6959
|
+
* contract that it still holds something servable.
|
|
6960
|
+
*
|
|
6961
|
+
* Only runs when the doc-root IS the build context — `outputDirectory` empty or
|
|
6962
|
+
* "." — which is the zero-build "just files, index.html at the root" project. A
|
|
6963
|
+
* real output directory (`dist`, `build`) is written by the build and contains
|
|
6964
|
+
* none of this, so the prune is a no-op there.
|
|
6965
|
+
*
|
|
6966
|
+
* Two distinct problems, one fix. The build context is pruned by `.dockerignore`
|
|
6967
|
+
* and THEN the generated recipe is written into it, so for these projects the
|
|
6968
|
+
* published site contained `.dockerignore` + `Dockerfile.openship…` — and any
|
|
6969
|
+
* tracked `.git` — at their own public URLs. Worse, those files count as
|
|
6970
|
+
* content: a repo with an allowlist `.dockerignore` (`*` + `!src`) extracted to
|
|
6971
|
+
* a tree holding ONLY the two build files, which passed the non-empty check and
|
|
6972
|
+
* deployed green as a site that 404s every request. Pruning first and
|
|
6973
|
+
* re-checking after is what makes that check mean "there is something to serve".
|
|
6974
|
+
*/
|
|
6975
|
+
async pruneBuildFilesFromDocRoot(config, hostOutDir, sshExecutor, log) {
|
|
6976
|
+
if (staticBuilderOutputPath(config) !== builderContextRoot(config)) return;
|
|
6977
|
+
if (sshExecutor) {
|
|
6978
|
+
const globs = _DockerRuntime.DOC_ROOT_EXCLUDED.map((name) => `${sq(hostOutDir)}/${name}`).join(
|
|
6979
|
+
" "
|
|
6980
|
+
);
|
|
6981
|
+
await sshExecutor.exec(`rm -rf ${globs}`).catch(() => {
|
|
6982
|
+
});
|
|
6983
|
+
log.log(`Excluded build files from the published output (${hostOutDir}).
|
|
6984
|
+
`);
|
|
6985
|
+
const listing = (await sshExecutor.exec(`ls -A ${sq(hostOutDir)} | head -1`).catch(() => "x")).trim();
|
|
6986
|
+
this.assertDocRootServable(!listing, hostOutDir);
|
|
6987
|
+
return;
|
|
6988
|
+
}
|
|
6989
|
+
const { readdir, rm } = await import("fs/promises");
|
|
6990
|
+
const { join: join3 } = await import("path");
|
|
6991
|
+
const entries = await readdir(hostOutDir).catch(() => null);
|
|
6992
|
+
if (entries === null) return;
|
|
6993
|
+
const doomed = entries.filter((name) => isExcludedDocRootEntry(name));
|
|
6994
|
+
for (const name of doomed) {
|
|
6995
|
+
await rm(join3(hostOutDir, name), { recursive: true, force: true }).catch(() => {
|
|
6996
|
+
});
|
|
6997
|
+
}
|
|
6998
|
+
if (doomed.length > 0) {
|
|
6999
|
+
log.log(`Excluded build files from the published output: ${doomed.join(", ")}
|
|
7000
|
+
`);
|
|
7001
|
+
}
|
|
7002
|
+
this.assertDocRootServable(entries.length === doomed.length, hostOutDir);
|
|
7003
|
+
}
|
|
7004
|
+
/** Post-prune half of the doc-root contract: build inputs are not content. */
|
|
7005
|
+
assertDocRootServable(isEmpty, hostOutDir) {
|
|
7006
|
+
if (!isEmpty) return;
|
|
7007
|
+
throw new Error(
|
|
7008
|
+
`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.`
|
|
7009
|
+
);
|
|
7010
|
+
}
|
|
6695
7011
|
assertDocRootFilled(isEmpty, docRoot) {
|
|
6696
7012
|
if (!isEmpty) return;
|
|
6697
7013
|
throw new Error(
|
|
@@ -6783,7 +7099,9 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6783
7099
|
`static extract failed copying ${docRoot} out of the build container (exit ${status.StatusCode})${logs ? `: ${logs.slice(-500)}` : ""}`
|
|
6784
7100
|
);
|
|
6785
7101
|
}
|
|
6786
|
-
const entries = (await readdir(hostOutDir).catch(() => [])).filter(
|
|
7102
|
+
const entries = (await readdir(hostOutDir).catch(() => [])).filter(
|
|
7103
|
+
(e) => e !== probe
|
|
7104
|
+
);
|
|
6787
7105
|
this.assertDocRootFilled(entries.length === 0, docRoot);
|
|
6788
7106
|
return true;
|
|
6789
7107
|
} finally {
|
|
@@ -6878,7 +7196,25 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6878
7196
|
prepareLogger.step("clone", "completed", "Source cloned on the server");
|
|
6879
7197
|
} else {
|
|
6880
7198
|
prepareLogger.step("clone", "running", "Preparing shared build context...");
|
|
6881
|
-
tree = await prepareSourceTree(source, {
|
|
7199
|
+
tree = await prepareSourceTree(source, {
|
|
7200
|
+
onLog: prepareLogger.callback,
|
|
7201
|
+
// Any service building from its OWN `build:` directory makes the source
|
|
7202
|
+
// root's `.dockerignore` the wrong file to prune this SHARED tree with —
|
|
7203
|
+
// it would delete files the other contexts need. Those builds filter their
|
|
7204
|
+
// own context instead; see SourceTree.dockerignorePruned.
|
|
7205
|
+
//
|
|
7206
|
+
// A malformed value throws here; `true` is the safe answer for it, because
|
|
7207
|
+
// it only skips a destructive prune. Letting the throw out would fail the
|
|
7208
|
+
// WHOLE batch, while per-spec resolution below reports it against the one
|
|
7209
|
+
// service that owns it.
|
|
7210
|
+
perServiceBuildContexts: specs.some((spec) => {
|
|
7211
|
+
try {
|
|
7212
|
+
return Boolean(dockerBuildContextDirectory(spec.config));
|
|
7213
|
+
} catch {
|
|
7214
|
+
return true;
|
|
7215
|
+
}
|
|
7216
|
+
})
|
|
7217
|
+
});
|
|
6882
7218
|
}
|
|
6883
7219
|
const resolvedList = await Promise.all(
|
|
6884
7220
|
specs.map(async (spec) => {
|
|
@@ -6886,34 +7222,54 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6886
7222
|
const requireRepo = spec.requireRepositoryDockerfile ?? spec.config.stack === "docker";
|
|
6887
7223
|
try {
|
|
6888
7224
|
if (cloneOnServer) {
|
|
6889
|
-
const dockerfileName = await this.resolveRemoteDockerfile(
|
|
7225
|
+
const { remoteBuildDir, dockerfileName } = await this.resolveRemoteDockerfile(
|
|
6890
7226
|
spec.config,
|
|
6891
7227
|
remoteContextDir,
|
|
6892
7228
|
generatedName,
|
|
6893
|
-
requireRepo
|
|
7229
|
+
requireRepo,
|
|
7230
|
+
spec.logger
|
|
6894
7231
|
);
|
|
6895
7232
|
return {
|
|
6896
7233
|
spec,
|
|
6897
7234
|
dockerfileName,
|
|
7235
|
+
remoteBuildSubdir: dockerBuildContextDirectory(spec.config),
|
|
7236
|
+
remoteBuildDir,
|
|
7237
|
+
buildContextDir: null,
|
|
6898
7238
|
contextEntries: null,
|
|
7239
|
+
ignoreContextPath: void 0,
|
|
7240
|
+
// Only the dockerode branch consults it, and clone-on-server never
|
|
7241
|
+
// reaches that branch (it is SSH by construction).
|
|
7242
|
+
requiresBuildKit: false,
|
|
6899
7243
|
error: null
|
|
6900
7244
|
};
|
|
6901
7245
|
}
|
|
6902
7246
|
const resolved = await resolveServiceDockerfile(tree.contextDir, spec.config, {
|
|
6903
7247
|
requireRepositoryDockerfile: requireRepo,
|
|
6904
|
-
generatedName
|
|
7248
|
+
generatedName,
|
|
7249
|
+
dockerignorePruned: tree.dockerignorePruned,
|
|
7250
|
+
onLog: spec.logger.callback
|
|
6905
7251
|
});
|
|
6906
7252
|
return {
|
|
6907
7253
|
spec,
|
|
6908
7254
|
dockerfileName: resolved.dockerfileName,
|
|
7255
|
+
remoteBuildSubdir: resolved.contextSubdir,
|
|
7256
|
+
remoteBuildDir: resolved.contextSubdir ? `${remoteContextDir}/${resolved.contextSubdir}` : remoteContextDir,
|
|
7257
|
+
buildContextDir: resolved.buildContextDir,
|
|
6909
7258
|
contextEntries: resolved.contextEntries,
|
|
7259
|
+
ignoreContextPath: resolved.ignoreContextPath,
|
|
7260
|
+
requiresBuildKit: resolved.requiresBuildKit,
|
|
6910
7261
|
error: null
|
|
6911
7262
|
};
|
|
6912
7263
|
} catch (err) {
|
|
6913
7264
|
return {
|
|
6914
7265
|
spec,
|
|
6915
7266
|
dockerfileName: null,
|
|
7267
|
+
remoteBuildSubdir: "",
|
|
7268
|
+
remoteBuildDir: remoteContextDir,
|
|
7269
|
+
buildContextDir: null,
|
|
6916
7270
|
contextEntries: null,
|
|
7271
|
+
ignoreContextPath: void 0,
|
|
7272
|
+
requiresBuildKit: false,
|
|
6917
7273
|
error: safeErrorMessage(err)
|
|
6918
7274
|
};
|
|
6919
7275
|
}
|
|
@@ -6935,7 +7291,17 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6935
7291
|
}
|
|
6936
7292
|
}
|
|
6937
7293
|
const results = [];
|
|
6938
|
-
for (const {
|
|
7294
|
+
for (const {
|
|
7295
|
+
spec,
|
|
7296
|
+
dockerfileName,
|
|
7297
|
+
remoteBuildDir,
|
|
7298
|
+
remoteBuildSubdir,
|
|
7299
|
+
buildContextDir,
|
|
7300
|
+
contextEntries,
|
|
7301
|
+
ignoreContextPath,
|
|
7302
|
+
requiresBuildKit,
|
|
7303
|
+
error
|
|
7304
|
+
} of resolvedList) {
|
|
6939
7305
|
const startedAt = Date.now();
|
|
6940
7306
|
const tag = this.imageTag(spec.config.slug, spec.config.sessionId);
|
|
6941
7307
|
const signal = abortControllers.get(spec.config.sessionId)?.signal;
|
|
@@ -6958,21 +7324,27 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6958
7324
|
}
|
|
6959
7325
|
spec.onStart?.();
|
|
6960
7326
|
try {
|
|
7327
|
+
if (remoteBuildSubdir) {
|
|
7328
|
+
spec.logger.log(`Using Docker build context: ${remoteBuildSubdir}
|
|
7329
|
+
`);
|
|
7330
|
+
}
|
|
6961
7331
|
if (isSsh) {
|
|
6962
7332
|
await this.buildImageOnRemote(
|
|
6963
7333
|
spec.config,
|
|
6964
|
-
|
|
7334
|
+
remoteBuildDir,
|
|
6965
7335
|
dockerfileName,
|
|
6966
7336
|
tag,
|
|
6967
7337
|
spec.logger,
|
|
6968
|
-
signal
|
|
7338
|
+
{ signal, requiresBuildKit }
|
|
6969
7339
|
);
|
|
6970
7340
|
} else {
|
|
6971
7341
|
const { body, abortSignal, takeError } = packBuildContext(
|
|
6972
|
-
tree.contextDir,
|
|
7342
|
+
buildContextDir ?? tree.contextDir,
|
|
6973
7343
|
contextEntries ?? [],
|
|
6974
|
-
signal
|
|
7344
|
+
signal,
|
|
7345
|
+
ignoreContextPath
|
|
6975
7346
|
);
|
|
7347
|
+
const builder = this.selectDockerodeBuilder(spec.config, requiresBuildKit, spec.logger);
|
|
6976
7348
|
try {
|
|
6977
7349
|
const stream = await this.docker.buildImage(body, {
|
|
6978
7350
|
t: tag,
|
|
@@ -6981,15 +7353,19 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6981
7353
|
projectId: spec.config.projectId,
|
|
6982
7354
|
sessionId: spec.config.sessionId
|
|
6983
7355
|
}),
|
|
6984
|
-
buildargs:
|
|
7356
|
+
buildargs: resolveDockerBuildArgs(spec.config),
|
|
6985
7357
|
...dockerBuildResourceLimits(spec.config.resources),
|
|
6986
7358
|
forcerm: true,
|
|
7359
|
+
...builder.options,
|
|
6987
7360
|
abortSignal
|
|
6988
7361
|
});
|
|
6989
|
-
await this.streamDockerodeBuild(stream, spec.logger);
|
|
7362
|
+
await this.streamDockerodeBuild(stream, spec.logger, builder.trace);
|
|
6990
7363
|
} catch (err) {
|
|
6991
7364
|
const contextErr = takeError();
|
|
6992
|
-
throw contextErr ? new Error(
|
|
7365
|
+
throw contextErr ? new Error(
|
|
7366
|
+
`Failed to read Docker build context: ${safeErrorMessage(contextErr)}`,
|
|
7367
|
+
{ cause: contextErr }
|
|
7368
|
+
) : err;
|
|
6993
7369
|
}
|
|
6994
7370
|
}
|
|
6995
7371
|
if (isCancelled(spec.config.sessionId)) {
|
|
@@ -7072,10 +7448,10 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7072
7448
|
}
|
|
7073
7449
|
const containers = await this.docker.listContainers({
|
|
7074
7450
|
all: true,
|
|
7075
|
-
filters: { label: [
|
|
7451
|
+
filters: { label: [OPENSHIP_LABEL.build] }
|
|
7076
7452
|
});
|
|
7077
7453
|
for (const c of containers) {
|
|
7078
|
-
const buildId = c.Labels?.[
|
|
7454
|
+
const buildId = c.Labels?.[OPENSHIP_LABEL.build];
|
|
7079
7455
|
if (!buildId || !cancelCovers(sessionId, buildId)) continue;
|
|
7080
7456
|
try {
|
|
7081
7457
|
await this.docker.getContainer(c.Id).remove({ force: true });
|
|
@@ -7096,10 +7472,18 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7096
7472
|
throw new Error("Docker deploy requires an imageRef (built image tag)");
|
|
7097
7473
|
}
|
|
7098
7474
|
const containerName = `openship-${config.runtimeName || config.projectId}-${config.deploymentId}`;
|
|
7475
|
+
const projectEnv = splitRuntimeEnv(config.envVars);
|
|
7476
|
+
if (projectEnv.dropped.length > 0) {
|
|
7477
|
+
log({
|
|
7478
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7479
|
+
level: "warn",
|
|
7480
|
+
message: droppedRuntimeEnvMessage(projectEnv.dropped)
|
|
7481
|
+
});
|
|
7482
|
+
}
|
|
7099
7483
|
const env = [
|
|
7100
7484
|
...config.portless ? [] : [`PORT=${config.port}`],
|
|
7101
7485
|
`NODE_ENV=${config.environment === "production" ? "production" : "development"}`,
|
|
7102
|
-
...
|
|
7486
|
+
...projectEnv.entries.map(([k, v]) => `${k}=${v}`)
|
|
7103
7487
|
];
|
|
7104
7488
|
const cmd = config.startCommand ? ["sh", "-c", config.startCommand] : void 0;
|
|
7105
7489
|
const restartPolicy = resolveRestartPolicy(config.restartPolicy);
|
|
@@ -7217,6 +7601,11 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7217
7601
|
await container.restart();
|
|
7218
7602
|
}
|
|
7219
7603
|
async removeImage(imageRef) {
|
|
7604
|
+
if (isArtifactPathRef(imageRef)) {
|
|
7605
|
+
throw new Error(
|
|
7606
|
+
`"${imageRef}" is a host directory, not an image reference \u2014 remove it with destroy().`
|
|
7607
|
+
);
|
|
7608
|
+
}
|
|
7220
7609
|
const image = this.docker.getImage(imageRef);
|
|
7221
7610
|
try {
|
|
7222
7611
|
await image.remove({ force: true });
|
|
@@ -7225,16 +7614,18 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7225
7614
|
}
|
|
7226
7615
|
}
|
|
7227
7616
|
async destroy(containerId) {
|
|
7228
|
-
if (containerId
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7617
|
+
if (isArtifactPathRef(containerId)) {
|
|
7618
|
+
if (this.transport.kind === "ssh") {
|
|
7619
|
+
const executor = this.connectionOptions?.executor;
|
|
7620
|
+
if (!executor) {
|
|
7621
|
+
throw new Error(
|
|
7622
|
+
`Cannot remove ${containerId}: the SSH transport has no command executor, and removing it locally would delete the wrong machine's files.`
|
|
7623
|
+
);
|
|
7624
|
+
}
|
|
7625
|
+
await removeManagedArtifact(executor, containerId);
|
|
7626
|
+
return;
|
|
7237
7627
|
}
|
|
7628
|
+
await removeManagedArtifact(null, containerId);
|
|
7238
7629
|
return;
|
|
7239
7630
|
}
|
|
7240
7631
|
const container = this.docker.getContainer(containerId);
|
|
@@ -7274,8 +7665,8 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7274
7665
|
return images.map((img) => ({
|
|
7275
7666
|
id: img.Id,
|
|
7276
7667
|
repoTags: (img.RepoTags ?? []).filter((t) => t && t !== "<none>:<none>"),
|
|
7277
|
-
buildId: img.Labels?.[
|
|
7278
|
-
deploymentId: img.Labels?.[
|
|
7668
|
+
buildId: img.Labels?.[OPENSHIP_LABEL.build],
|
|
7669
|
+
deploymentId: img.Labels?.[OPENSHIP_LABEL.deployment],
|
|
7279
7670
|
size: img.Size ?? 0
|
|
7280
7671
|
}));
|
|
7281
7672
|
}
|
|
@@ -7320,7 +7711,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7320
7711
|
return containers.map((c) => ({
|
|
7321
7712
|
containerId: c.Id,
|
|
7322
7713
|
status: stateMap[(c.State ?? "").toLowerCase().trim()] ?? "stopped",
|
|
7323
|
-
serviceName: c.Labels?.[
|
|
7714
|
+
serviceName: c.Labels?.[OPENSHIP_LABEL.service]
|
|
7324
7715
|
}));
|
|
7325
7716
|
}
|
|
7326
7717
|
// ── Rollback primitives (retention half only) ────────────────────────
|
|
@@ -7349,17 +7740,18 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7349
7740
|
}
|
|
7350
7741
|
}
|
|
7351
7742
|
async purge(deployment) {
|
|
7743
|
+
const failures = [];
|
|
7352
7744
|
if (deployment.containerId) {
|
|
7353
|
-
|
|
7354
|
-
await this.destroy(deployment.containerId);
|
|
7355
|
-
} catch {
|
|
7356
|
-
}
|
|
7745
|
+
await this.destroy(deployment.containerId).catch((err) => failures.push(err));
|
|
7357
7746
|
}
|
|
7358
7747
|
if (deployment.imageRef && ownsBuiltImage(deployment.imageRef)) {
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
|
|
7362
|
-
|
|
7748
|
+
await this.removeImage(deployment.imageRef).catch((err) => failures.push(err));
|
|
7749
|
+
}
|
|
7750
|
+
if (failures.length === 1) throw failures[0];
|
|
7751
|
+
if (failures.length > 1) {
|
|
7752
|
+
throw new Error(
|
|
7753
|
+
failures.map((err) => err instanceof Error ? err.message : String(err)).join("; ")
|
|
7754
|
+
);
|
|
7363
7755
|
}
|
|
7364
7756
|
}
|
|
7365
7757
|
/**
|
|
@@ -7367,7 +7759,9 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7367
7759
|
* the ones that survive `container.remove()` and would otherwise leak.
|
|
7368
7760
|
* Anonymous volumes are auto-removed with `{ v: true }` and don't need to
|
|
7369
7761
|
* be enumerated. Bind mounts and tmpfs are skipped (the user manages them
|
|
7370
|
-
* outside our control). Returns [] if the container is already gone
|
|
7762
|
+
* outside our control). Returns [] only if the container is already gone;
|
|
7763
|
+
* transport and permission failures propagate so an explicit volume wipe
|
|
7764
|
+
* never destroys the last copy of its mount inventory.
|
|
7371
7765
|
*/
|
|
7372
7766
|
async inspectNamedVolumes(containerId) {
|
|
7373
7767
|
try {
|
|
@@ -7375,16 +7769,18 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7375
7769
|
const data = await container.inspect();
|
|
7376
7770
|
const mounts = data.Mounts ?? [];
|
|
7377
7771
|
return mounts.filter((m) => m.Type === "volume" && typeof m.Name === "string" && m.Name.length > 0).map((m) => m.Name);
|
|
7378
|
-
} catch {
|
|
7379
|
-
return [];
|
|
7772
|
+
} catch (err) {
|
|
7773
|
+
if (isDockerNotFoundError(err)) return [];
|
|
7774
|
+
throw err;
|
|
7380
7775
|
}
|
|
7381
7776
|
}
|
|
7382
|
-
/** Remove a named volume by name.
|
|
7777
|
+
/** Remove a named volume by name. Already-gone is idempotent success. */
|
|
7383
7778
|
async removeVolume(name) {
|
|
7384
7779
|
try {
|
|
7385
7780
|
const volume = this.docker.getVolume(name);
|
|
7386
7781
|
await volume.remove({ force: true });
|
|
7387
|
-
} catch {
|
|
7782
|
+
} catch (err) {
|
|
7783
|
+
if (!isDockerNotFoundError(err)) throw err;
|
|
7388
7784
|
}
|
|
7389
7785
|
}
|
|
7390
7786
|
// ── Docker discovery (label-agnostic) ────────────────────────────────────
|
|
@@ -7598,7 +7994,8 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7598
7994
|
const executor = this.transport.kind === "ssh" ? this.connectionOptions?.executor : null;
|
|
7599
7995
|
if (executor?.execWithInput) {
|
|
7600
7996
|
const { code, stderr, stdout } = await executor.execWithInput(`docker load`, body);
|
|
7601
|
-
if (code !== 0)
|
|
7997
|
+
if (code !== 0)
|
|
7998
|
+
throw new Error(`docker load exited ${code}${stderr ? `: ${stderr.slice(0, 500)}` : ""}`);
|
|
7602
7999
|
return parseLoadedImageRef(stdout);
|
|
7603
8000
|
}
|
|
7604
8001
|
const stream = await this.docker.loadImage(body);
|
|
@@ -7677,7 +8074,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7677
8074
|
};
|
|
7678
8075
|
const startedAt = data.State.StartedAt;
|
|
7679
8076
|
const uptimeSeconds = startedAt && data.State.Running ? Math.floor((Date.now() - new Date(startedAt).getTime()) / 1e3) : void 0;
|
|
7680
|
-
const { ip, hostPort } = extractNetworkInfo(data);
|
|
8077
|
+
const { ip, hostPort, hostPortByContainerPort } = extractNetworkInfo(data);
|
|
7681
8078
|
let status;
|
|
7682
8079
|
if (data.State.Running) {
|
|
7683
8080
|
status = "running";
|
|
@@ -7692,6 +8089,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7692
8089
|
status,
|
|
7693
8090
|
ip,
|
|
7694
8091
|
hostPort,
|
|
8092
|
+
...hostPortByContainerPort ? { hostPortByContainerPort } : {},
|
|
7695
8093
|
uptimeSeconds: uptimeSeconds && uptimeSeconds > 0 ? uptimeSeconds : void 0
|
|
7696
8094
|
};
|
|
7697
8095
|
}
|
|
@@ -7855,7 +8253,11 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7855
8253
|
});
|
|
7856
8254
|
stream.on("end", () => {
|
|
7857
8255
|
if (buffer && !destroyed) {
|
|
7858
|
-
onLog({
|
|
8256
|
+
onLog({
|
|
8257
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8258
|
+
message: buffer,
|
|
8259
|
+
level: parseLogLevel(buffer)
|
|
8260
|
+
});
|
|
7859
8261
|
buffer = "";
|
|
7860
8262
|
}
|
|
7861
8263
|
});
|
|
@@ -8070,15 +8472,9 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8070
8472
|
async inContainerExecutor(containerId) {
|
|
8071
8473
|
return {
|
|
8072
8474
|
exec: async (command, opts) => {
|
|
8073
|
-
const { exitCode, stdout, stderr } = await this.execInContainer(
|
|
8074
|
-
containerId,
|
|
8075
|
-
command,
|
|
8076
|
-
opts
|
|
8077
|
-
);
|
|
8475
|
+
const { exitCode, stdout, stderr } = await this.execInContainer(containerId, command, opts);
|
|
8078
8476
|
if (exitCode !== 0) {
|
|
8079
|
-
throw new Error(
|
|
8080
|
-
stdout.trim() || stderr.trim() || `exec exited with code ${exitCode}`
|
|
8081
|
-
);
|
|
8477
|
+
throw new Error(stdout.trim() || stderr.trim() || `exec exited with code ${exitCode}`);
|
|
8082
8478
|
}
|
|
8083
8479
|
return stdout;
|
|
8084
8480
|
}
|
|
@@ -8177,12 +8573,13 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8177
8573
|
const ownNames = /* @__PURE__ */ new Set();
|
|
8178
8574
|
const foreign = /* @__PURE__ */ new Map();
|
|
8179
8575
|
for (const c of containers) {
|
|
8180
|
-
const owner = c.Labels?.[
|
|
8576
|
+
const owner = c.Labels?.[OPENSHIP_LABEL.project];
|
|
8181
8577
|
if (!owner) continue;
|
|
8182
8578
|
for (const m of c.Mounts ?? []) {
|
|
8183
8579
|
if (m.Type !== "volume" || !m.Name || !named.has(m.Name)) continue;
|
|
8184
8580
|
if (owner === config.projectId) ownNames.add(m.Name);
|
|
8185
|
-
else if (!foreign.has(m.Name))
|
|
8581
|
+
else if (!foreign.has(m.Name))
|
|
8582
|
+
foreign.set(m.Name, c.Names?.[0]?.replace(/^\//, "") ?? owner);
|
|
8186
8583
|
}
|
|
8187
8584
|
}
|
|
8188
8585
|
for (const [name, other] of foreign) {
|
|
@@ -8231,7 +8628,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8231
8628
|
);
|
|
8232
8629
|
if (onNetwork) continue;
|
|
8233
8630
|
if (this.cannotJoinNetworks(c)) continue;
|
|
8234
|
-
const service = c.Labels?.[
|
|
8631
|
+
const service = c.Labels?.[OPENSHIP_LABEL.service];
|
|
8235
8632
|
try {
|
|
8236
8633
|
await network.connect({
|
|
8237
8634
|
Container: c.Id,
|
|
@@ -8255,7 +8652,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8255
8652
|
* doesn't exist (source not deployed) is skipped and nothing here ever throws —
|
|
8256
8653
|
* a link networking failure must never fail the consumer's deploy.
|
|
8257
8654
|
*/
|
|
8258
|
-
async attachToExternalNetworks(projectId, networkNames) {
|
|
8655
|
+
async attachToExternalNetworks(projectId, networkNames, extraContainerIds = []) {
|
|
8259
8656
|
if (networkNames.length === 0) return;
|
|
8260
8657
|
let containers;
|
|
8261
8658
|
try {
|
|
@@ -8266,6 +8663,22 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8266
8663
|
} catch {
|
|
8267
8664
|
return;
|
|
8268
8665
|
}
|
|
8666
|
+
if (extraContainerIds.length > 0) {
|
|
8667
|
+
const seen = new Set(containers.map((c) => c.Id));
|
|
8668
|
+
for (const id of extraContainerIds) {
|
|
8669
|
+
if (seen.has(id)) continue;
|
|
8670
|
+
try {
|
|
8671
|
+
const info = await this.docker.getContainer(id).inspect();
|
|
8672
|
+
containers.push({
|
|
8673
|
+
Id: info.Id,
|
|
8674
|
+
NetworkSettings: { Networks: info.NetworkSettings?.Networks ?? {} },
|
|
8675
|
+
HostConfig: { NetworkMode: info.HostConfig?.NetworkMode }
|
|
8676
|
+
});
|
|
8677
|
+
seen.add(info.Id);
|
|
8678
|
+
} catch {
|
|
8679
|
+
}
|
|
8680
|
+
}
|
|
8681
|
+
}
|
|
8269
8682
|
for (const name of networkNames) {
|
|
8270
8683
|
const network = this.docker.getNetwork(name);
|
|
8271
8684
|
let netId;
|
|
@@ -8286,21 +8699,20 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8286
8699
|
} catch (err) {
|
|
8287
8700
|
const msg = err?.message ?? "";
|
|
8288
8701
|
if (!/already exists|already connected/i.test(msg)) {
|
|
8289
|
-
console.warn(
|
|
8290
|
-
`[docker] link-connect failed for ${c.Id.slice(0, 12)} \u2192 ${name}: ${msg}`
|
|
8291
|
-
);
|
|
8702
|
+
console.warn(`[docker] link-connect failed for ${c.Id.slice(0, 12)} \u2192 ${name}: ${msg}`);
|
|
8292
8703
|
}
|
|
8293
8704
|
}
|
|
8294
8705
|
}
|
|
8295
8706
|
}
|
|
8296
8707
|
}
|
|
8297
|
-
/** Remove a project network
|
|
8708
|
+
/** Remove a project network. Already-gone is idempotent success. */
|
|
8298
8709
|
async removeNetwork(slug) {
|
|
8299
8710
|
const networkName = `openship-${slug}`;
|
|
8300
8711
|
try {
|
|
8301
8712
|
const network = this.docker.getNetwork(networkName);
|
|
8302
8713
|
await network.remove();
|
|
8303
|
-
} catch {
|
|
8714
|
+
} catch (err) {
|
|
8715
|
+
if (!isDockerNotFoundError(err)) throw err;
|
|
8304
8716
|
}
|
|
8305
8717
|
}
|
|
8306
8718
|
/**
|
|
@@ -8319,9 +8731,17 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8319
8731
|
await existing.remove({ force: true });
|
|
8320
8732
|
} catch {
|
|
8321
8733
|
}
|
|
8734
|
+
const serviceEnv = splitRuntimeEnv(config.environment);
|
|
8735
|
+
if (serviceEnv.dropped.length > 0) {
|
|
8736
|
+
log({
|
|
8737
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8738
|
+
level: "warn",
|
|
8739
|
+
message: `[${config.serviceName}] ${droppedRuntimeEnvMessage(serviceEnv.dropped)}`
|
|
8740
|
+
});
|
|
8741
|
+
}
|
|
8322
8742
|
const env = [
|
|
8323
8743
|
...config.publicPort && config.environment.PORT === void 0 ? [`PORT=${config.publicPort}`] : [],
|
|
8324
|
-
...
|
|
8744
|
+
...serviceEnv.entries.map(([k, v]) => `${k}=${v}`)
|
|
8325
8745
|
];
|
|
8326
8746
|
const cmd = resolveComposeCmd(config);
|
|
8327
8747
|
const entrypoint = resolveComposeEntrypoint(config.advanced);
|
|
@@ -8405,7 +8825,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8405
8825
|
deploymentId: config.deploymentId,
|
|
8406
8826
|
projectId: config.projectId
|
|
8407
8827
|
}),
|
|
8408
|
-
|
|
8828
|
+
[OPENSHIP_LABEL.service]: config.serviceName
|
|
8409
8829
|
},
|
|
8410
8830
|
...healthcheck && { Healthcheck: healthcheck },
|
|
8411
8831
|
...stopConfig,
|
|
@@ -8444,7 +8864,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8444
8864
|
throw startErr;
|
|
8445
8865
|
}
|
|
8446
8866
|
const data = await container.inspect();
|
|
8447
|
-
const { ip, hostPort } = extractNetworkInfo(data);
|
|
8867
|
+
const { ip, hostPort, hostPortByContainerPort } = extractNetworkInfo(data);
|
|
8448
8868
|
const imageDigest = await this.resolveImageDigest(config.image).catch(() => void 0);
|
|
8449
8869
|
log({
|
|
8450
8870
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -8457,6 +8877,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8457
8877
|
status: "running",
|
|
8458
8878
|
ip,
|
|
8459
8879
|
hostPort,
|
|
8880
|
+
...hostPortByContainerPort ? { hostPortByContainerPort } : {},
|
|
8460
8881
|
imageDigest
|
|
8461
8882
|
};
|
|
8462
8883
|
}
|
|
@@ -8467,6 +8888,16 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8467
8888
|
* RepoDigests) or any inspect error. Works local + SSH (plain inspect, no
|
|
8468
8889
|
* followProgress). Never throws.
|
|
8469
8890
|
*/
|
|
8891
|
+
/**
|
|
8892
|
+
* PUBLIC because it is the only anchor the update scanner has.
|
|
8893
|
+
*
|
|
8894
|
+
* `resolveDeployedDrift` reads `service_deployment.image_digest` to tell a moved mutable
|
|
8895
|
+
* tag (`:latest`, `:16`) from an unchanged one — a REF comparison can't, since the ref
|
|
8896
|
+
* string is identical either way. A deploy records it here; an ADOPTED stack records
|
|
8897
|
+
* nothing, and an adopted stack is image-mode by definition (registry images, no repo),
|
|
8898
|
+
* so every migrated `postgres:16`/`redis:7` project was told "up to date" forever. The
|
|
8899
|
+
* adopt paths need to read it for a container they did not create.
|
|
8900
|
+
*/
|
|
8470
8901
|
async resolveImageDigest(ref) {
|
|
8471
8902
|
const info = await this.docker.getImage(ref).inspect();
|
|
8472
8903
|
const repoDigests = info?.RepoDigests ?? [];
|
|
@@ -8483,7 +8914,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8483
8914
|
};
|
|
8484
8915
|
|
|
8485
8916
|
export {
|
|
8486
|
-
|
|
8917
|
+
ensureScopedVolumeName,
|
|
8487
8918
|
isHostPathSource,
|
|
8488
8919
|
startExecStream,
|
|
8489
8920
|
startAttachStream,
|