openship 0.6.5 → 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-2ANV5ZQ5.js +88 -0
- package/dist/{chunk-LEYSL46B.js → chunk-2VSW6Q6C.js} +3911 -898
- package/dist/{chunk-5OUWIPOB.js → chunk-5T4UVOM3.js} +16 -86
- package/dist/{chunk-EZPPTWJQ.js → chunk-BLEXTYRK.js} +1 -1
- package/dist/{chunk-3ZAGC5NT.js → chunk-CIJV43LH.js} +83 -33
- package/dist/{chunk-7S4USQYF.js → chunk-DDFUESPJ.js} +67 -124
- package/dist/{chunk-6FH4AXVY.js → chunk-E2X26PXR.js} +1 -1
- package/dist/{chunk-XVOBL7BC.js → chunk-FYPFEDPZ.js} +1 -1
- package/dist/{chunk-WFRXYWK2.js → chunk-FZ6QCF6B.js} +8 -6
- package/dist/{chunk-VHMOQL5Y.js → chunk-GGIAIGUL.js} +125 -80
- package/dist/{chunk-WE7BXW6L.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-HPXP6LXZ.js → chunk-J2XM3QSP.js} +8 -5
- package/dist/{chunk-6JELW75A.js → chunk-J3K6XABC.js} +5 -5
- package/dist/{chunk-VDESJSG6.js → chunk-LHFXSC36.js} +558 -109
- package/dist/{chunk-MSZUCNPV.js → chunk-MEE6OZFP.js} +3 -3
- package/dist/chunk-N5YZMO33.js +111 -0
- package/dist/{chunk-VLQ3UIVO.js → chunk-NRTJRNAE.js} +105 -37
- package/dist/chunk-PPEBIY5F.js +2331 -0
- package/dist/{chunk-RFWJE7ZN.js → chunk-PVP4RGWT.js} +3 -3
- package/dist/{chunk-PZRRL5PC.js → chunk-QHMDCYZ3.js} +15 -4
- package/dist/chunk-R4VDPFLS.js +106 -0
- package/dist/chunk-RDXT4BQB.js +310 -0
- package/dist/{chunk-GRTXMUHA.js → chunk-S6B2Z3OO.js} +145 -60
- package/dist/chunk-UWEV6XTS.js +249 -0
- package/dist/{chunk-QLX7NBEA.js → chunk-VQM6JHJB.js} +1004 -2046
- package/dist/{chunk-A7EWM2KA.js → chunk-VSTFHUCV.js} +44 -50
- package/dist/chunk-WU54BXFE.js +70 -0
- package/dist/{chunk-LFCSC4IU.js → chunk-XWHWTHEL.js} +313 -469
- package/dist/{chunk-7334NSJL.js → chunk-ZPJ74LXE.js} +76 -38
- package/dist/cloud-KNNABE3O.js +40 -0
- package/dist/{detect-P4CIQBRH.js → detect-G4DR3U2N.js} +9 -6
- package/dist/{docker-3K2NEBD6.js → docker-YOVPCIB4.js} +11 -7
- package/dist/{docker-edge-executor-YZZ6P7IC.js → docker-edge-executor-QFXD3337.js} +8 -6
- package/dist/edge-import-CRFVSR6P.js +56 -0
- package/dist/{edge-preflight-ES6FH4JK.js → edge-preflight-MXERVAMG.js} +15 -12
- package/dist/{ensure-container-edge-YPITNH45.js → ensure-container-edge-F4JUNQIK.js} +13 -10
- package/dist/{executor-DZGX55K2.js → executor-CVPAYYHR.js} +10 -5
- package/dist/host-channel-preflight-QTDNGLCK.js +54 -0
- package/dist/index.js +139 -92
- package/dist/{local-executor-WOFZD72X.js → local-executor-ODC6RANU.js} +3 -2
- package/dist/{nginx-NSAHWGJV.js → nginx-KLFF43TC.js} +13 -8
- package/dist/{openresty-lua-DR4D3EZ6.js → openresty-lua-VKWUV4GB.js} +8 -5
- package/dist/{privilege-YTVI4SKF.js → privilege-7D3SSOL3.js} +6 -5
- package/dist/server/engine/samples/amavisd/amavisd.conf +25 -0
- package/dist/server/engine/samples/postfix/master.cf +14 -0
- package/dist/server/index.js +44526 -33476
- package/dist/server/migrations/0105_mail_inbound_rules.sql +80 -0
- package/dist/server/migrations/0106_mcp_call_tracking.sql +23 -0
- package/dist/server/migrations/0107_build_minute_metering_indexes.sql +20 -0
- package/dist/server/migrations/0108_credential.sql +68 -0
- package/dist/server/migrations/meta/_journal.json +28 -0
- package/dist/setup-I5AJC6NJ.js +25 -0
- package/dist/ssh-client-625VWGKP.js +29 -0
- package/dist/up-QXTOOVXC.js +61 -0
- package/package.json +1 -1
- package/dist/bare-XQIPDO4R.js +0 -24
- package/dist/chunk-6HOA5JT7.js +0 -124
- package/dist/chunk-TM2IQPYU.js +0 -434
- package/dist/chunk-XRNRT37W.js +0 -316
- package/dist/cloud-3DODOWTR.js +0 -36
- package/dist/edge-import-MCJNVTZ2.js +0 -51
- package/dist/host-channel-preflight-MUXE3KZU.js +0 -49
- package/dist/setup-WUUZURHS.js +0 -22
- package/dist/up-XEO3SR6Z.js +0 -58
- /package/dist/{dist-es-ZP5QDSJ3.js → dist-es-AXFRS7PS.js} +0 -0
|
@@ -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,21 +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,
|
|
62
|
+
registryConfigKeys,
|
|
63
|
+
registryForImage,
|
|
44
64
|
safeErrorMessage,
|
|
45
65
|
withTimeout
|
|
46
|
-
} from "./chunk-
|
|
66
|
+
} from "./chunk-2VSW6Q6C.js";
|
|
47
67
|
import {
|
|
48
68
|
__commonJS,
|
|
49
69
|
__require,
|
|
@@ -313,7 +333,7 @@ var require_buffer_list = __commonJS({
|
|
|
313
333
|
}
|
|
314
334
|
}, {
|
|
315
335
|
key: "join",
|
|
316
|
-
value: function
|
|
336
|
+
value: function join3(s) {
|
|
317
337
|
if (this.length === 0) return "";
|
|
318
338
|
var p = this.head;
|
|
319
339
|
var ret = "" + p.data;
|
|
@@ -4607,6 +4627,83 @@ var tarFs = __toESM(require_tar_fs(), 1);
|
|
|
4607
4627
|
import Dockerode from "dockerode";
|
|
4608
4628
|
import { createGzip } from "zlib";
|
|
4609
4629
|
import { PassThrough, Writable } from "stream";
|
|
4630
|
+
import { relative, sep } from "path";
|
|
4631
|
+
|
|
4632
|
+
// ../../packages/adapters/src/runtime/docker-auth.ts
|
|
4633
|
+
import { readFile } from "fs/promises";
|
|
4634
|
+
import { homedir } from "os";
|
|
4635
|
+
import { join } from "path";
|
|
4636
|
+
function firstPresent(values, keys) {
|
|
4637
|
+
if (!values) return void 0;
|
|
4638
|
+
for (const key of keys) {
|
|
4639
|
+
if (Object.hasOwn(values, key)) return { key, value: values[key] };
|
|
4640
|
+
}
|
|
4641
|
+
return void 0;
|
|
4642
|
+
}
|
|
4643
|
+
function dockerConfigJsonFor(auth) {
|
|
4644
|
+
const a = auth;
|
|
4645
|
+
const registry = a.serveraddress?.trim();
|
|
4646
|
+
if (!registry) return null;
|
|
4647
|
+
if (a.identitytoken) {
|
|
4648
|
+
return JSON.stringify({ auths: { [registry]: { identitytoken: a.identitytoken } } });
|
|
4649
|
+
}
|
|
4650
|
+
if (!a.username || !a.password) return null;
|
|
4651
|
+
const encoded = Buffer.from(`${a.username}:${a.password}`).toString("base64");
|
|
4652
|
+
return JSON.stringify({ auths: { [registry]: { auth: encoded } } });
|
|
4653
|
+
}
|
|
4654
|
+
function toAuthConfig(entry, serveraddress) {
|
|
4655
|
+
if (entry.identitytoken) {
|
|
4656
|
+
return { identitytoken: entry.identitytoken, serveraddress };
|
|
4657
|
+
}
|
|
4658
|
+
if (entry.username && entry.password) {
|
|
4659
|
+
return { username: entry.username, password: entry.password, serveraddress };
|
|
4660
|
+
}
|
|
4661
|
+
if (!entry.auth) return void 0;
|
|
4662
|
+
let decoded;
|
|
4663
|
+
try {
|
|
4664
|
+
decoded = Buffer.from(entry.auth, "base64").toString("utf8");
|
|
4665
|
+
} catch {
|
|
4666
|
+
return void 0;
|
|
4667
|
+
}
|
|
4668
|
+
const separator = decoded.indexOf(":");
|
|
4669
|
+
if (separator < 1) return void 0;
|
|
4670
|
+
return {
|
|
4671
|
+
username: decoded.slice(0, separator),
|
|
4672
|
+
password: decoded.slice(separator + 1),
|
|
4673
|
+
serveraddress
|
|
4674
|
+
};
|
|
4675
|
+
}
|
|
4676
|
+
var DockerCredentialHelperError = class extends Error {
|
|
4677
|
+
constructor(registry) {
|
|
4678
|
+
super(
|
|
4679
|
+
`Openship can't read the credentials for ${registry}: they are stored in a Docker credential helper, which is not available inside the Openship container. Add the credentials inline to the Docker config (docker logout ${registry}, remove the credential helper for it, then docker login ${registry}), or use a public image.`
|
|
4680
|
+
);
|
|
4681
|
+
this.name = "DockerCredentialHelperError";
|
|
4682
|
+
}
|
|
4683
|
+
};
|
|
4684
|
+
async function resolveDockerAuth(ref) {
|
|
4685
|
+
const registry = registryForImage(ref);
|
|
4686
|
+
const configPath = join(
|
|
4687
|
+
process.env.DOCKER_CONFIG || join(homedir(), ".docker"),
|
|
4688
|
+
"config.json"
|
|
4689
|
+
);
|
|
4690
|
+
let config;
|
|
4691
|
+
try {
|
|
4692
|
+
config = JSON.parse(await readFile(configPath, "utf8"));
|
|
4693
|
+
} catch {
|
|
4694
|
+
return void 0;
|
|
4695
|
+
}
|
|
4696
|
+
if (!config || typeof config !== "object") return void 0;
|
|
4697
|
+
const keys = registryConfigKeys(registry);
|
|
4698
|
+
const inline = firstPresent(config.auths, keys);
|
|
4699
|
+
const usable = inline ? toAuthConfig(inline.value, inline.key) : void 0;
|
|
4700
|
+
if (usable) return usable;
|
|
4701
|
+
const helperForRegistry = firstPresent(config.credHelpers, keys);
|
|
4702
|
+
if (helperForRegistry || config.credsStore && inline) {
|
|
4703
|
+
throw new DockerCredentialHelperError(registry);
|
|
4704
|
+
}
|
|
4705
|
+
return void 0;
|
|
4706
|
+
}
|
|
4610
4707
|
|
|
4611
4708
|
// ../../packages/adapters/src/runtime/source-tarball.ts
|
|
4612
4709
|
function githubTarballUrl(repoUrl, ref) {
|
|
@@ -4634,6 +4731,10 @@ var MODE_SUFFIX = /:(ro|rw|z|Z|nocopy)$/;
|
|
|
4634
4731
|
function scopedVolumeName(slug, name) {
|
|
4635
4732
|
return `openship-${slug}-${name}`;
|
|
4636
4733
|
}
|
|
4734
|
+
function ensureScopedVolumeName(slug, source) {
|
|
4735
|
+
const prefix = `openship-${slug}-`;
|
|
4736
|
+
return source.startsWith(prefix) ? source : scopedVolumeName(slug, source);
|
|
4737
|
+
}
|
|
4637
4738
|
function isHostPathSource(source) {
|
|
4638
4739
|
return source.startsWith("/") || source.startsWith("./") || source.startsWith("../") || source.startsWith("~");
|
|
4639
4740
|
}
|
|
@@ -4792,6 +4893,9 @@ function resolveComposeCmd(config) {
|
|
|
4792
4893
|
if (config.commandArgv != null) return config.commandArgv;
|
|
4793
4894
|
return config.command ? ["sh", "-c", config.command] : void 0;
|
|
4794
4895
|
}
|
|
4896
|
+
function resolveComposeEntrypoint(advanced) {
|
|
4897
|
+
return advanced?.entrypoint ?? void 0;
|
|
4898
|
+
}
|
|
4795
4899
|
|
|
4796
4900
|
// ../../packages/adapters/src/runtime/resource-limits.ts
|
|
4797
4901
|
function dockerResourceLimits(resources) {
|
|
@@ -4843,8 +4947,8 @@ function describeResourceLimits(resources) {
|
|
|
4843
4947
|
// ../../packages/adapters/src/runtime/docker-transport.ts
|
|
4844
4948
|
import { createHash } from "crypto";
|
|
4845
4949
|
import { readFileSync } from "fs";
|
|
4846
|
-
import { homedir } from "os";
|
|
4847
|
-
import { join } from "path";
|
|
4950
|
+
import { homedir as homedir2 } from "os";
|
|
4951
|
+
import { join as join2 } from "path";
|
|
4848
4952
|
|
|
4849
4953
|
// ../../packages/adapters/src/runtime/docker-ssh-agent.ts
|
|
4850
4954
|
import net2 from "net";
|
|
@@ -5317,7 +5421,7 @@ function createDockerSshBridge(opts) {
|
|
|
5317
5421
|
var DEFAULT_DOCKER_SOCKET_PATH = "/var/run/docker.sock";
|
|
5318
5422
|
var contextSocketCache = /* @__PURE__ */ new Map();
|
|
5319
5423
|
function resolveDockerContextSocket(env) {
|
|
5320
|
-
const configDir = env.DOCKER_CONFIG?.trim() ||
|
|
5424
|
+
const configDir = env.DOCKER_CONFIG?.trim() || join2(homedir2(), ".docker");
|
|
5321
5425
|
let name = env.DOCKER_CONTEXT?.trim();
|
|
5322
5426
|
const cacheKey = `${configDir}\0${name ?? ""}`;
|
|
5323
5427
|
const cached = contextSocketCache.get(cacheKey);
|
|
@@ -5325,13 +5429,13 @@ function resolveDockerContextSocket(env) {
|
|
|
5325
5429
|
let socket = null;
|
|
5326
5430
|
try {
|
|
5327
5431
|
if (!name) {
|
|
5328
|
-
const config = JSON.parse(readFileSync(
|
|
5432
|
+
const config = JSON.parse(readFileSync(join2(configDir, "config.json"), "utf8"));
|
|
5329
5433
|
name = typeof config.currentContext === "string" ? config.currentContext.trim() : "";
|
|
5330
5434
|
}
|
|
5331
5435
|
if (name && name !== "default") {
|
|
5332
5436
|
const digest = createHash("sha256").update(name).digest("hex");
|
|
5333
5437
|
const meta = JSON.parse(
|
|
5334
|
-
readFileSync(
|
|
5438
|
+
readFileSync(join2(configDir, "contexts", "meta", digest, "meta.json"), "utf8")
|
|
5335
5439
|
);
|
|
5336
5440
|
const host = meta.Endpoints?.docker?.Host;
|
|
5337
5441
|
if (typeof host === "string" && host.startsWith("unix://")) {
|
|
@@ -5620,9 +5724,10 @@ function parsePortBindings(portSpecs) {
|
|
|
5620
5724
|
}
|
|
5621
5725
|
return { exposedPorts, portBindings };
|
|
5622
5726
|
}
|
|
5623
|
-
function packBuildContext(contextDir, entries, cancelSignal) {
|
|
5727
|
+
function packBuildContext(contextDir, entries, cancelSignal, ignoreContextPath) {
|
|
5624
5728
|
const controller = new AbortController();
|
|
5625
|
-
const
|
|
5729
|
+
const ignore = ignoreContextPath ? (absolutePath) => ignoreContextPath(relative(contextDir, absolutePath).split(sep).join("/")) : void 0;
|
|
5730
|
+
const pack2 = tarFs.pack(contextDir, { entries, ignore });
|
|
5626
5731
|
const body = pack2.pipe(createGzip());
|
|
5627
5732
|
let contextError = null;
|
|
5628
5733
|
const capture = (err) => {
|
|
@@ -5754,14 +5859,22 @@ function extractNetworkInfo(data) {
|
|
|
5754
5859
|
break;
|
|
5755
5860
|
}
|
|
5756
5861
|
}
|
|
5862
|
+
const hostPortByContainerPort = {};
|
|
5757
5863
|
let hostPort;
|
|
5758
|
-
for (const bindings of Object.
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
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;
|
|
5763
5872
|
}
|
|
5764
|
-
return {
|
|
5873
|
+
return {
|
|
5874
|
+
ip,
|
|
5875
|
+
hostPort,
|
|
5876
|
+
...Object.keys(hostPortByContainerPort).length ? { hostPortByContainerPort } : {}
|
|
5877
|
+
};
|
|
5765
5878
|
}
|
|
5766
5879
|
function parseLoadedImageRef(output) {
|
|
5767
5880
|
const matches = [...(output || "").matchAll(/Loaded image(?: ID)?:\s*(\S+)/gi)];
|
|
@@ -5918,11 +6031,11 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
5918
6031
|
/** Labels applied to both build images and deploy containers. */
|
|
5919
6032
|
labels(config) {
|
|
5920
6033
|
const l = {
|
|
5921
|
-
|
|
6034
|
+
[OPENSHIP_LABEL.project]: config.projectId
|
|
5922
6035
|
};
|
|
5923
|
-
if (config.deploymentId) l[
|
|
5924
|
-
if (config.sessionId) l[
|
|
5925
|
-
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;
|
|
5926
6039
|
return l;
|
|
5927
6040
|
}
|
|
5928
6041
|
// ── Build lifecycle ────────────────────────────────────────────────────
|
|
@@ -5933,7 +6046,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
5933
6046
|
*/
|
|
5934
6047
|
async estimateContextSize(dir) {
|
|
5935
6048
|
const { stat, readdir } = await import("fs/promises");
|
|
5936
|
-
const { join:
|
|
6049
|
+
const { join: join3 } = await import("path");
|
|
5937
6050
|
let total = 0;
|
|
5938
6051
|
const stack = [dir];
|
|
5939
6052
|
while (stack.length) {
|
|
@@ -5945,7 +6058,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
5945
6058
|
continue;
|
|
5946
6059
|
}
|
|
5947
6060
|
for (const entry of entries) {
|
|
5948
|
-
const full =
|
|
6061
|
+
const full = join3(current, entry.name);
|
|
5949
6062
|
if (entry.isDirectory()) {
|
|
5950
6063
|
stack.push(full);
|
|
5951
6064
|
} else if (entry.isFile()) {
|
|
@@ -5964,12 +6077,20 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
5964
6077
|
logger.log(`[${step}] ${label} ${message}`);
|
|
5965
6078
|
logger.step(step, status, message);
|
|
5966
6079
|
}
|
|
5967
|
-
handleBuildEvent(event, logger) {
|
|
6080
|
+
handleBuildEvent(event, logger, trace) {
|
|
5968
6081
|
const errorMessage = event.errorDetail?.message ?? event.error;
|
|
5969
6082
|
if (errorMessage) {
|
|
5970
6083
|
logger.log(errorMessage, "error");
|
|
5971
6084
|
return errorMessage;
|
|
5972
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
|
+
}
|
|
5973
6094
|
if (event.stream) {
|
|
5974
6095
|
const line = event.stream.trim();
|
|
5975
6096
|
if (!line) return null;
|
|
@@ -6026,6 +6147,12 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6026
6147
|
if (/returned a non-zero code:\s*\d+/i.test(line)) {
|
|
6027
6148
|
return line;
|
|
6028
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
|
+
}
|
|
6029
6156
|
if (/\/workspace\/package\.json/i.test(line) && /ENOENT/i.test(line)) {
|
|
6030
6157
|
return "Docker build ran from /workspace but package.json was not found there. The configured rootDirectory is likely empty or incorrect.";
|
|
6031
6158
|
}
|
|
@@ -6077,12 +6204,35 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6077
6204
|
{ excludes: [] }
|
|
6078
6205
|
);
|
|
6079
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
|
+
}
|
|
6080
6227
|
/**
|
|
6081
6228
|
* Run native `docker build` on the remote host against an already-transferred
|
|
6082
|
-
* context dir. One image; `dockerfileName` selects which Dockerfile
|
|
6083
|
-
*
|
|
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).
|
|
6084
6233
|
*/
|
|
6085
|
-
async buildImageOnRemote(config,
|
|
6234
|
+
async buildImageOnRemote(config, remoteBuildDir, dockerfileName, tag, log, opts) {
|
|
6235
|
+
const signal = opts?.signal;
|
|
6086
6236
|
const executor = this.connectionOptions?.executor;
|
|
6087
6237
|
if (!executor) throw new Error("SSH build path requires an executor on connectionOptions");
|
|
6088
6238
|
const buildArgs = Object.entries({
|
|
@@ -6093,7 +6243,22 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6093
6243
|
this.labels({ projectId: config.projectId, sessionId: config.sessionId })
|
|
6094
6244
|
).map(([k, v]) => `--label ${sq(`${k}=${v}`)}`).join(" ");
|
|
6095
6245
|
const dockerfileFlag = dockerfileName && dockerfileName !== "Dockerfile" ? ` -f ${sq(dockerfileName)}` : "";
|
|
6096
|
-
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} .`;
|
|
6097
6262
|
log.log(`Running on remote: ${buildCmd}`);
|
|
6098
6263
|
log.log("\u2500\u2500\u2500 docker build output \u2500\u2500\u2500");
|
|
6099
6264
|
this.emitDockerStep(log, "install", "running", "Running install inside container (docker build)");
|
|
@@ -6205,15 +6370,50 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6205
6370
|
* (clone-on-server). Mirrors resolveServiceDockerfile but probes the remote
|
|
6206
6371
|
* tree with `test -f` instead of the local FS: a repository Dockerfile
|
|
6207
6372
|
* candidate is used when present; otherwise a Dockerfile is generated locally
|
|
6208
|
-
* (pure fn) and written to the remote tree.
|
|
6209
|
-
*
|
|
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.
|
|
6210
6379
|
*/
|
|
6211
|
-
async resolveRemoteDockerfile(config, remoteContextDir, generatedName, requireRepositoryDockerfile) {
|
|
6380
|
+
async resolveRemoteDockerfile(config, remoteContextDir, generatedName, requireRepositoryDockerfile, log) {
|
|
6212
6381
|
const executor = this.connectionOptions?.executor;
|
|
6213
6382
|
if (!executor) throw new Error("Clone-on-server requires an SSH executor on connectionOptions");
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
|
|
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
|
+
);
|
|
6217
6417
|
}
|
|
6218
6418
|
if (requireRepositoryDockerfile) {
|
|
6219
6419
|
const expected = config.dockerfilePath?.trim() || "Dockerfile";
|
|
@@ -6222,7 +6422,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6222
6422
|
);
|
|
6223
6423
|
}
|
|
6224
6424
|
await executor.writeFile(`${remoteContextDir}/${generatedName}`, generateDockerfile(config));
|
|
6225
|
-
return generatedName;
|
|
6425
|
+
return { remoteBuildDir, dockerfileName: generatedName };
|
|
6226
6426
|
}
|
|
6227
6427
|
async buildViaSshTarPipe(config, buildContext, tag, log, signal) {
|
|
6228
6428
|
const remoteContextDir = `/tmp/openship-build-${config.sessionId}`;
|
|
@@ -6230,11 +6430,11 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6230
6430
|
await this.transferBuildContext(buildContext.contextDir, remoteContextDir, log);
|
|
6231
6431
|
await this.buildImageOnRemote(
|
|
6232
6432
|
config,
|
|
6233
|
-
remoteContextDir,
|
|
6433
|
+
buildContext.contextSubdir ? `${remoteContextDir}/${buildContext.contextSubdir}` : remoteContextDir,
|
|
6234
6434
|
buildContext.dockerfileName,
|
|
6235
6435
|
tag,
|
|
6236
6436
|
log,
|
|
6237
|
-
signal
|
|
6437
|
+
{ signal, requiresBuildKit: buildContext.requiresBuildKit }
|
|
6238
6438
|
);
|
|
6239
6439
|
} finally {
|
|
6240
6440
|
this.connectionOptions?.executor?.exec(`rm -rf ${sq(remoteContextDir)}`).catch(() => {
|
|
@@ -6242,6 +6442,41 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6242
6442
|
await buildContext.cleanup();
|
|
6243
6443
|
}
|
|
6244
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
|
+
}
|
|
6245
6480
|
/**
|
|
6246
6481
|
* Dockerode build path. Used for local socket and TCP transports, plus
|
|
6247
6482
|
* SSH transports that didn't get an executor wired in (shouldn't happen
|
|
@@ -6254,10 +6489,12 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6254
6489
|
async buildViaDockerode(config, buildContext, tag, log, cancelSignal) {
|
|
6255
6490
|
log.log(`Streaming build context to Docker daemon - image tag: ${tag}`);
|
|
6256
6491
|
const { body, abortSignal, takeError } = packBuildContext(
|
|
6257
|
-
buildContext.
|
|
6492
|
+
buildContext.buildContextDir,
|
|
6258
6493
|
buildContext.contextEntries,
|
|
6259
|
-
cancelSignal
|
|
6494
|
+
cancelSignal,
|
|
6495
|
+
buildContext.ignoreContextPath
|
|
6260
6496
|
);
|
|
6497
|
+
const builder = this.selectDockerodeBuilder(config, buildContext.requiresBuildKit, log);
|
|
6261
6498
|
try {
|
|
6262
6499
|
const stream = await this.docker.buildImage(body, {
|
|
6263
6500
|
t: tag,
|
|
@@ -6272,10 +6509,11 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6272
6509
|
// needs several GB). Opt-in only; see dockerBuildResourceLimits.
|
|
6273
6510
|
...dockerBuildResourceLimits(config.resources),
|
|
6274
6511
|
forcerm: true,
|
|
6512
|
+
...builder.options,
|
|
6275
6513
|
abortSignal
|
|
6276
6514
|
});
|
|
6277
6515
|
log.log("Connected to Docker daemon. Build output follows:");
|
|
6278
|
-
await this.streamDockerodeBuild(stream, log);
|
|
6516
|
+
await this.streamDockerodeBuild(stream, log, builder.trace);
|
|
6279
6517
|
log.log("Docker daemon finished streaming build output. Finalizing image...\n");
|
|
6280
6518
|
} catch (err) {
|
|
6281
6519
|
const contextErr = takeError();
|
|
@@ -6289,7 +6527,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6289
6527
|
* idle timeout + keepalive, and throw on a fatal build event. Shared by the
|
|
6290
6528
|
* single-image dockerode path and the local batch build (buildImages).
|
|
6291
6529
|
*/
|
|
6292
|
-
async streamDockerodeBuild(stream, log) {
|
|
6530
|
+
async streamDockerodeBuild(stream, log, trace) {
|
|
6293
6531
|
let fatalBuildError = null;
|
|
6294
6532
|
await new Promise((resolve, reject) => {
|
|
6295
6533
|
let settled = false;
|
|
@@ -6346,7 +6584,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6346
6584
|
},
|
|
6347
6585
|
(event) => {
|
|
6348
6586
|
resetIdleTimer();
|
|
6349
|
-
fatalBuildError ??= this.handleBuildEvent(event, log);
|
|
6587
|
+
fatalBuildError ??= this.handleBuildEvent(event, log, trace);
|
|
6350
6588
|
}
|
|
6351
6589
|
);
|
|
6352
6590
|
});
|
|
@@ -6415,20 +6653,16 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6415
6653
|
await this.cloneSourceOnRemote(config, remoteContextDir, log);
|
|
6416
6654
|
if (abort.signal.aborted) throw new BuildCancelledError();
|
|
6417
6655
|
this.emitDockerStep(log, "clone", "completed", "Source cloned on the server");
|
|
6418
|
-
const dockerfileName = await this.resolveRemoteDockerfile(
|
|
6656
|
+
const { remoteBuildDir, dockerfileName } = await this.resolveRemoteDockerfile(
|
|
6419
6657
|
config,
|
|
6420
6658
|
remoteContextDir,
|
|
6421
6659
|
"Dockerfile.openship",
|
|
6422
|
-
config.stack === "docker"
|
|
6423
|
-
|
|
6424
|
-
await this.buildImageOnRemote(
|
|
6425
|
-
config,
|
|
6426
|
-
remoteContextDir,
|
|
6427
|
-
dockerfileName,
|
|
6428
|
-
tag,
|
|
6429
|
-
log,
|
|
6430
|
-
abort.signal
|
|
6660
|
+
config.stack === "docker",
|
|
6661
|
+
log
|
|
6431
6662
|
);
|
|
6663
|
+
await this.buildImageOnRemote(config, remoteBuildDir, dockerfileName, tag, log, {
|
|
6664
|
+
signal: abort.signal
|
|
6665
|
+
});
|
|
6432
6666
|
} finally {
|
|
6433
6667
|
sshExecutor.exec(`rm -rf ${sq(remoteContextDir)}`).catch(() => {
|
|
6434
6668
|
});
|
|
@@ -6458,7 +6692,9 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6458
6692
|
} catch {
|
|
6459
6693
|
this.emitDockerStep(log, "clone", "completed", "Docker build context ready");
|
|
6460
6694
|
}
|
|
6461
|
-
if (buildContext.
|
|
6695
|
+
if (buildContext.contextSubdir) {
|
|
6696
|
+
log.log(`Using Docker build context: ${buildContext.contextSubdir}`);
|
|
6697
|
+
} else if (buildContext.rootDirectory) {
|
|
6462
6698
|
log.log(`Using Docker build root: ${buildContext.rootDirectory}`);
|
|
6463
6699
|
}
|
|
6464
6700
|
if (buildContext.usesRepositoryDockerfile) {
|
|
@@ -6527,6 +6763,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6527
6763
|
await this.extractDocRootViaDaemon(tag, docRoot, hostOutDir);
|
|
6528
6764
|
}
|
|
6529
6765
|
}
|
|
6766
|
+
await this.pruneBuildFilesFromDocRoot(config, hostOutDir, sshExecutor ?? null, log);
|
|
6530
6767
|
log.log(`Static files ready at ${hostOutDir}
|
|
6531
6768
|
`);
|
|
6532
6769
|
} finally {
|
|
@@ -6611,6 +6848,74 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6611
6848
|
* expensive one: every extractor can succeed having copied nothing, which deploys
|
|
6612
6849
|
* green and then 404s the entire site.
|
|
6613
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
|
+
}
|
|
6614
6919
|
assertDocRootFilled(isEmpty, docRoot) {
|
|
6615
6920
|
if (!isEmpty) return;
|
|
6616
6921
|
throw new Error(
|
|
@@ -6663,12 +6968,12 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6663
6968
|
*/
|
|
6664
6969
|
async extractDocRootViaBindMount(tag, docRoot, hostOutDir) {
|
|
6665
6970
|
const { mkdir, readdir, writeFile, rm } = await import("fs/promises");
|
|
6666
|
-
const { join:
|
|
6971
|
+
const { join: join3 } = await import("path");
|
|
6667
6972
|
const { randomBytes } = await import("crypto");
|
|
6668
6973
|
await mkdir(hostOutDir, { recursive: true });
|
|
6669
6974
|
const OUT = "/__openship_out";
|
|
6670
6975
|
const probe = `.openship-extract-probe-${randomBytes(6).toString("hex")}`;
|
|
6671
|
-
await writeFile(
|
|
6976
|
+
await writeFile(join3(hostOutDir, probe), "probe");
|
|
6672
6977
|
const container = await this.docker.createContainer({
|
|
6673
6978
|
Image: tag,
|
|
6674
6979
|
// Override the ENTRYPOINT: builder base images (node, bun) ship a
|
|
@@ -6708,7 +7013,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6708
7013
|
} finally {
|
|
6709
7014
|
await container.remove({ force: true }).catch(() => {
|
|
6710
7015
|
});
|
|
6711
|
-
await rm(
|
|
7016
|
+
await rm(join3(hostOutDir, probe), { force: true }).catch(() => {
|
|
6712
7017
|
});
|
|
6713
7018
|
}
|
|
6714
7019
|
}
|
|
@@ -6797,7 +7102,25 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6797
7102
|
prepareLogger.step("clone", "completed", "Source cloned on the server");
|
|
6798
7103
|
} else {
|
|
6799
7104
|
prepareLogger.step("clone", "running", "Preparing shared build context...");
|
|
6800
|
-
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
|
+
});
|
|
6801
7124
|
}
|
|
6802
7125
|
const resolvedList = await Promise.all(
|
|
6803
7126
|
specs.map(async (spec) => {
|
|
@@ -6805,34 +7128,54 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6805
7128
|
const requireRepo = spec.requireRepositoryDockerfile ?? spec.config.stack === "docker";
|
|
6806
7129
|
try {
|
|
6807
7130
|
if (cloneOnServer) {
|
|
6808
|
-
const dockerfileName = await this.resolveRemoteDockerfile(
|
|
7131
|
+
const { remoteBuildDir, dockerfileName } = await this.resolveRemoteDockerfile(
|
|
6809
7132
|
spec.config,
|
|
6810
7133
|
remoteContextDir,
|
|
6811
7134
|
generatedName,
|
|
6812
|
-
requireRepo
|
|
7135
|
+
requireRepo,
|
|
7136
|
+
spec.logger
|
|
6813
7137
|
);
|
|
6814
7138
|
return {
|
|
6815
7139
|
spec,
|
|
6816
7140
|
dockerfileName,
|
|
7141
|
+
remoteBuildSubdir: dockerBuildContextDirectory(spec.config),
|
|
7142
|
+
remoteBuildDir,
|
|
7143
|
+
buildContextDir: null,
|
|
6817
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,
|
|
6818
7149
|
error: null
|
|
6819
7150
|
};
|
|
6820
7151
|
}
|
|
6821
7152
|
const resolved = await resolveServiceDockerfile(tree.contextDir, spec.config, {
|
|
6822
7153
|
requireRepositoryDockerfile: requireRepo,
|
|
6823
|
-
generatedName
|
|
7154
|
+
generatedName,
|
|
7155
|
+
dockerignorePruned: tree.dockerignorePruned,
|
|
7156
|
+
onLog: spec.logger.callback
|
|
6824
7157
|
});
|
|
6825
7158
|
return {
|
|
6826
7159
|
spec,
|
|
6827
7160
|
dockerfileName: resolved.dockerfileName,
|
|
7161
|
+
remoteBuildSubdir: resolved.contextSubdir,
|
|
7162
|
+
remoteBuildDir: resolved.contextSubdir ? `${remoteContextDir}/${resolved.contextSubdir}` : remoteContextDir,
|
|
7163
|
+
buildContextDir: resolved.buildContextDir,
|
|
6828
7164
|
contextEntries: resolved.contextEntries,
|
|
7165
|
+
ignoreContextPath: resolved.ignoreContextPath,
|
|
7166
|
+
requiresBuildKit: resolved.requiresBuildKit,
|
|
6829
7167
|
error: null
|
|
6830
7168
|
};
|
|
6831
7169
|
} catch (err) {
|
|
6832
7170
|
return {
|
|
6833
7171
|
spec,
|
|
6834
7172
|
dockerfileName: null,
|
|
7173
|
+
remoteBuildSubdir: "",
|
|
7174
|
+
remoteBuildDir: remoteContextDir,
|
|
7175
|
+
buildContextDir: null,
|
|
6835
7176
|
contextEntries: null,
|
|
7177
|
+
ignoreContextPath: void 0,
|
|
7178
|
+
requiresBuildKit: false,
|
|
6836
7179
|
error: safeErrorMessage(err)
|
|
6837
7180
|
};
|
|
6838
7181
|
}
|
|
@@ -6854,7 +7197,17 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6854
7197
|
}
|
|
6855
7198
|
}
|
|
6856
7199
|
const results = [];
|
|
6857
|
-
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) {
|
|
6858
7211
|
const startedAt = Date.now();
|
|
6859
7212
|
const tag = this.imageTag(spec.config.slug, spec.config.sessionId);
|
|
6860
7213
|
const signal = abortControllers.get(spec.config.sessionId)?.signal;
|
|
@@ -6877,20 +7230,30 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6877
7230
|
}
|
|
6878
7231
|
spec.onStart?.();
|
|
6879
7232
|
try {
|
|
7233
|
+
if (remoteBuildSubdir) {
|
|
7234
|
+
spec.logger.log(`Using Docker build context: ${remoteBuildSubdir}
|
|
7235
|
+
`);
|
|
7236
|
+
}
|
|
6880
7237
|
if (isSsh) {
|
|
6881
7238
|
await this.buildImageOnRemote(
|
|
6882
7239
|
spec.config,
|
|
6883
|
-
|
|
7240
|
+
remoteBuildDir,
|
|
6884
7241
|
dockerfileName,
|
|
6885
7242
|
tag,
|
|
6886
7243
|
spec.logger,
|
|
6887
|
-
signal
|
|
7244
|
+
{ signal, requiresBuildKit }
|
|
6888
7245
|
);
|
|
6889
7246
|
} else {
|
|
6890
7247
|
const { body, abortSignal, takeError } = packBuildContext(
|
|
6891
|
-
tree.contextDir,
|
|
7248
|
+
buildContextDir ?? tree.contextDir,
|
|
6892
7249
|
contextEntries ?? [],
|
|
6893
|
-
signal
|
|
7250
|
+
signal,
|
|
7251
|
+
ignoreContextPath
|
|
7252
|
+
);
|
|
7253
|
+
const builder = this.selectDockerodeBuilder(
|
|
7254
|
+
spec.config,
|
|
7255
|
+
requiresBuildKit,
|
|
7256
|
+
spec.logger
|
|
6894
7257
|
);
|
|
6895
7258
|
try {
|
|
6896
7259
|
const stream = await this.docker.buildImage(body, {
|
|
@@ -6903,9 +7266,10 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6903
7266
|
buildargs: { ...spec.config.envVars, NODE_ENV: "production" },
|
|
6904
7267
|
...dockerBuildResourceLimits(spec.config.resources),
|
|
6905
7268
|
forcerm: true,
|
|
7269
|
+
...builder.options,
|
|
6906
7270
|
abortSignal
|
|
6907
7271
|
});
|
|
6908
|
-
await this.streamDockerodeBuild(stream, spec.logger);
|
|
7272
|
+
await this.streamDockerodeBuild(stream, spec.logger, builder.trace);
|
|
6909
7273
|
} catch (err) {
|
|
6910
7274
|
const contextErr = takeError();
|
|
6911
7275
|
throw contextErr ? new Error(`Failed to read Docker build context: ${safeErrorMessage(contextErr)}`, { cause: contextErr }) : err;
|
|
@@ -6991,10 +7355,10 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
6991
7355
|
}
|
|
6992
7356
|
const containers = await this.docker.listContainers({
|
|
6993
7357
|
all: true,
|
|
6994
|
-
filters: { label: [
|
|
7358
|
+
filters: { label: [OPENSHIP_LABEL.build] }
|
|
6995
7359
|
});
|
|
6996
7360
|
for (const c of containers) {
|
|
6997
|
-
const buildId = c.Labels?.[
|
|
7361
|
+
const buildId = c.Labels?.[OPENSHIP_LABEL.build];
|
|
6998
7362
|
if (!buildId || !cancelCovers(sessionId, buildId)) continue;
|
|
6999
7363
|
try {
|
|
7000
7364
|
await this.docker.getContainer(c.Id).remove({ force: true });
|
|
@@ -7015,10 +7379,18 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7015
7379
|
throw new Error("Docker deploy requires an imageRef (built image tag)");
|
|
7016
7380
|
}
|
|
7017
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
|
+
}
|
|
7018
7390
|
const env = [
|
|
7019
7391
|
...config.portless ? [] : [`PORT=${config.port}`],
|
|
7020
7392
|
`NODE_ENV=${config.environment === "production" ? "production" : "development"}`,
|
|
7021
|
-
...
|
|
7393
|
+
...projectEnv.entries.map(([k, v]) => `${k}=${v}`)
|
|
7022
7394
|
];
|
|
7023
7395
|
const cmd = config.startCommand ? ["sh", "-c", config.startCommand] : void 0;
|
|
7024
7396
|
const restartPolicy = resolveRestartPolicy(config.restartPolicy);
|
|
@@ -7136,6 +7508,11 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7136
7508
|
await container.restart();
|
|
7137
7509
|
}
|
|
7138
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
|
+
}
|
|
7139
7516
|
const image = this.docker.getImage(imageRef);
|
|
7140
7517
|
try {
|
|
7141
7518
|
await image.remove({ force: true });
|
|
@@ -7144,16 +7521,18 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7144
7521
|
}
|
|
7145
7522
|
}
|
|
7146
7523
|
async destroy(containerId) {
|
|
7147
|
-
if (containerId
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
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;
|
|
7156
7534
|
}
|
|
7535
|
+
await removeManagedArtifact(null, containerId);
|
|
7157
7536
|
return;
|
|
7158
7537
|
}
|
|
7159
7538
|
const container = this.docker.getContainer(containerId);
|
|
@@ -7193,8 +7572,8 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7193
7572
|
return images.map((img) => ({
|
|
7194
7573
|
id: img.Id,
|
|
7195
7574
|
repoTags: (img.RepoTags ?? []).filter((t) => t && t !== "<none>:<none>"),
|
|
7196
|
-
buildId: img.Labels?.[
|
|
7197
|
-
deploymentId: img.Labels?.[
|
|
7575
|
+
buildId: img.Labels?.[OPENSHIP_LABEL.build],
|
|
7576
|
+
deploymentId: img.Labels?.[OPENSHIP_LABEL.deployment],
|
|
7198
7577
|
size: img.Size ?? 0
|
|
7199
7578
|
}));
|
|
7200
7579
|
}
|
|
@@ -7239,7 +7618,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7239
7618
|
return containers.map((c) => ({
|
|
7240
7619
|
containerId: c.Id,
|
|
7241
7620
|
status: stateMap[(c.State ?? "").toLowerCase().trim()] ?? "stopped",
|
|
7242
|
-
serviceName: c.Labels?.[
|
|
7621
|
+
serviceName: c.Labels?.[OPENSHIP_LABEL.service]
|
|
7243
7622
|
}));
|
|
7244
7623
|
}
|
|
7245
7624
|
// ── Rollback primitives (retention half only) ────────────────────────
|
|
@@ -7268,17 +7647,18 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7268
7647
|
}
|
|
7269
7648
|
}
|
|
7270
7649
|
async purge(deployment) {
|
|
7650
|
+
const failures = [];
|
|
7271
7651
|
if (deployment.containerId) {
|
|
7272
|
-
|
|
7273
|
-
await this.destroy(deployment.containerId);
|
|
7274
|
-
} catch {
|
|
7275
|
-
}
|
|
7652
|
+
await this.destroy(deployment.containerId).catch((err) => failures.push(err));
|
|
7276
7653
|
}
|
|
7277
7654
|
if (deployment.imageRef && ownsBuiltImage(deployment.imageRef)) {
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
|
|
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
|
+
);
|
|
7282
7662
|
}
|
|
7283
7663
|
}
|
|
7284
7664
|
/**
|
|
@@ -7425,13 +7805,39 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7425
7805
|
}
|
|
7426
7806
|
const executor = this.connectionOptions?.executor;
|
|
7427
7807
|
if (executor) {
|
|
7428
|
-
|
|
7808
|
+
const timeout = 10 * 6e4;
|
|
7809
|
+
const auth = await this.connectionOptions?.resolveRegistryAuth?.(ref);
|
|
7810
|
+
const config = auth ? dockerConfigJsonFor(auth) : null;
|
|
7811
|
+
if (!config) {
|
|
7812
|
+
await executor.exec(`docker pull ${sq(ref)}`, { timeout });
|
|
7813
|
+
return;
|
|
7814
|
+
}
|
|
7815
|
+
const dir = `/tmp/openship-pull-${crypto.randomUUID()}`;
|
|
7816
|
+
try {
|
|
7817
|
+
await executor.exec(`mkdir -m 700 ${sq(dir)}`);
|
|
7818
|
+
await executor.writeFile(`${dir}/config.json`, config);
|
|
7819
|
+
await executor.exec(`chmod 600 ${sq(dir)}/config.json`);
|
|
7820
|
+
await executor.exec(
|
|
7821
|
+
`DOCKER_CONFIG=${sq(dir)} docker pull ${sq(ref)}; rc=$?; rm -rf ${sq(dir)}; exit $rc`,
|
|
7822
|
+
{ timeout }
|
|
7823
|
+
);
|
|
7824
|
+
} finally {
|
|
7825
|
+
await executor.exec(`rm -rf ${sq(dir)}`).catch(() => {
|
|
7826
|
+
});
|
|
7827
|
+
}
|
|
7429
7828
|
return;
|
|
7430
7829
|
}
|
|
7431
|
-
const
|
|
7432
|
-
|
|
7433
|
-
this.docker.
|
|
7434
|
-
|
|
7830
|
+
const authconfig = this.connectionOptions?.resolveRegistryAuth ? await this.connectionOptions.resolveRegistryAuth(ref) : await resolveDockerAuth(ref);
|
|
7831
|
+
try {
|
|
7832
|
+
const stream = await this.docker.pull(ref, authconfig ? { authconfig } : {});
|
|
7833
|
+
await new Promise((resolve, reject) => {
|
|
7834
|
+
this.docker.modem.followProgress(stream, (err) => err ? reject(err) : resolve());
|
|
7835
|
+
});
|
|
7836
|
+
} catch (err) {
|
|
7837
|
+
throw new Error(
|
|
7838
|
+
`Failed to pull ${ref} from ${registryForImage(ref)}: ${safeErrorMessage(err)}`
|
|
7839
|
+
);
|
|
7840
|
+
}
|
|
7435
7841
|
}
|
|
7436
7842
|
/** Is this image tag present on THIS daemon? Distinguishes a locally-built
|
|
7437
7843
|
* image (must be transferred cross-server) from a registry tag (the target
|
|
@@ -7570,7 +7976,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7570
7976
|
};
|
|
7571
7977
|
const startedAt = data.State.StartedAt;
|
|
7572
7978
|
const uptimeSeconds = startedAt && data.State.Running ? Math.floor((Date.now() - new Date(startedAt).getTime()) / 1e3) : void 0;
|
|
7573
|
-
const { ip, hostPort } = extractNetworkInfo(data);
|
|
7979
|
+
const { ip, hostPort, hostPortByContainerPort } = extractNetworkInfo(data);
|
|
7574
7980
|
let status;
|
|
7575
7981
|
if (data.State.Running) {
|
|
7576
7982
|
status = "running";
|
|
@@ -7585,6 +7991,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
7585
7991
|
status,
|
|
7586
7992
|
ip,
|
|
7587
7993
|
hostPort,
|
|
7994
|
+
...hostPortByContainerPort ? { hostPortByContainerPort } : {},
|
|
7588
7995
|
uptimeSeconds: uptimeSeconds && uptimeSeconds > 0 ? uptimeSeconds : void 0
|
|
7589
7996
|
};
|
|
7590
7997
|
}
|
|
@@ -8070,7 +8477,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8070
8477
|
const ownNames = /* @__PURE__ */ new Set();
|
|
8071
8478
|
const foreign = /* @__PURE__ */ new Map();
|
|
8072
8479
|
for (const c of containers) {
|
|
8073
|
-
const owner = c.Labels?.[
|
|
8480
|
+
const owner = c.Labels?.[OPENSHIP_LABEL.project];
|
|
8074
8481
|
if (!owner) continue;
|
|
8075
8482
|
for (const m of c.Mounts ?? []) {
|
|
8076
8483
|
if (m.Type !== "volume" || !m.Name || !named.has(m.Name)) continue;
|
|
@@ -8124,7 +8531,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8124
8531
|
);
|
|
8125
8532
|
if (onNetwork) continue;
|
|
8126
8533
|
if (this.cannotJoinNetworks(c)) continue;
|
|
8127
|
-
const service = c.Labels?.[
|
|
8534
|
+
const service = c.Labels?.[OPENSHIP_LABEL.service];
|
|
8128
8535
|
try {
|
|
8129
8536
|
await network.connect({
|
|
8130
8537
|
Container: c.Id,
|
|
@@ -8148,7 +8555,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8148
8555
|
* doesn't exist (source not deployed) is skipped and nothing here ever throws —
|
|
8149
8556
|
* a link networking failure must never fail the consumer's deploy.
|
|
8150
8557
|
*/
|
|
8151
|
-
async attachToExternalNetworks(projectId, networkNames) {
|
|
8558
|
+
async attachToExternalNetworks(projectId, networkNames, extraContainerIds = []) {
|
|
8152
8559
|
if (networkNames.length === 0) return;
|
|
8153
8560
|
let containers;
|
|
8154
8561
|
try {
|
|
@@ -8159,6 +8566,22 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8159
8566
|
} catch {
|
|
8160
8567
|
return;
|
|
8161
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
|
+
}
|
|
8162
8585
|
for (const name of networkNames) {
|
|
8163
8586
|
const network = this.docker.getNetwork(name);
|
|
8164
8587
|
let netId;
|
|
@@ -8212,11 +8635,20 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8212
8635
|
await existing.remove({ force: true });
|
|
8213
8636
|
} catch {
|
|
8214
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
|
+
}
|
|
8215
8646
|
const env = [
|
|
8216
8647
|
...config.publicPort && config.environment.PORT === void 0 ? [`PORT=${config.publicPort}`] : [],
|
|
8217
|
-
...
|
|
8648
|
+
...serviceEnv.entries.map(([k, v]) => `${k}=${v}`)
|
|
8218
8649
|
];
|
|
8219
8650
|
const cmd = resolveComposeCmd(config);
|
|
8651
|
+
const entrypoint = resolveComposeEntrypoint(config.advanced);
|
|
8220
8652
|
const sharedNetwork = config.namespaces?.network;
|
|
8221
8653
|
const sharesNetns = !!sharedNetwork && sharedNetwork !== "none";
|
|
8222
8654
|
const ownsProjectEndpoint = ownsNetworkEndpoint(sharedNetwork);
|
|
@@ -8282,6 +8714,12 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8282
8714
|
name: containerName,
|
|
8283
8715
|
Image: config.image,
|
|
8284
8716
|
Cmd: cmd,
|
|
8717
|
+
// Entrypoint (#575). SPREAD rather than assigned, because `undefined` and `[]`
|
|
8718
|
+
// must reach Docker differently: absent leaves the image's own ENTRYPOINT
|
|
8719
|
+
// alone, while `Entrypoint: []` clears it. Assigning `undefined` would send the
|
|
8720
|
+
// key, and the same conditional-spread shape is why `toStopConfig` returns an
|
|
8721
|
+
// object instead of nullable fields.
|
|
8722
|
+
...entrypoint === void 0 ? {} : { Entrypoint: entrypoint },
|
|
8285
8723
|
Env: env,
|
|
8286
8724
|
// Docker rejects an explicit hostname when the network namespace is another
|
|
8287
8725
|
// container's — that container owns the UTS identity too.
|
|
@@ -8291,7 +8729,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8291
8729
|
deploymentId: config.deploymentId,
|
|
8292
8730
|
projectId: config.projectId
|
|
8293
8731
|
}),
|
|
8294
|
-
|
|
8732
|
+
[OPENSHIP_LABEL.service]: config.serviceName
|
|
8295
8733
|
},
|
|
8296
8734
|
...healthcheck && { Healthcheck: healthcheck },
|
|
8297
8735
|
...stopConfig,
|
|
@@ -8330,7 +8768,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8330
8768
|
throw startErr;
|
|
8331
8769
|
}
|
|
8332
8770
|
const data = await container.inspect();
|
|
8333
|
-
const { ip, hostPort } = extractNetworkInfo(data);
|
|
8771
|
+
const { ip, hostPort, hostPortByContainerPort } = extractNetworkInfo(data);
|
|
8334
8772
|
const imageDigest = await this.resolveImageDigest(config.image).catch(() => void 0);
|
|
8335
8773
|
log({
|
|
8336
8774
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -8343,6 +8781,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8343
8781
|
status: "running",
|
|
8344
8782
|
ip,
|
|
8345
8783
|
hostPort,
|
|
8784
|
+
...hostPortByContainerPort ? { hostPortByContainerPort } : {},
|
|
8346
8785
|
imageDigest
|
|
8347
8786
|
};
|
|
8348
8787
|
}
|
|
@@ -8353,6 +8792,16 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8353
8792
|
* RepoDigests) or any inspect error. Works local + SSH (plain inspect, no
|
|
8354
8793
|
* followProgress). Never throws.
|
|
8355
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
|
+
*/
|
|
8356
8805
|
async resolveImageDigest(ref) {
|
|
8357
8806
|
const info = await this.docker.getImage(ref).inspect();
|
|
8358
8807
|
const repoDigests = info?.RepoDigests ?? [];
|
|
@@ -8369,7 +8818,7 @@ var DockerRuntime = class _DockerRuntime {
|
|
|
8369
8818
|
};
|
|
8370
8819
|
|
|
8371
8820
|
export {
|
|
8372
|
-
|
|
8821
|
+
ensureScopedVolumeName,
|
|
8373
8822
|
isHostPathSource,
|
|
8374
8823
|
startExecStream,
|
|
8375
8824
|
startAttachStream,
|