signalk-container 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/containers.d.ts +53 -6
- package/dist/containers.d.ts.map +1 -1
- package/dist/containers.js +278 -165
- package/dist/containers.js.map +1 -1
- package/dist/doctor.d.ts +42 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +65 -0
- package/dist/doctor.js.map +1 -0
- package/dist/index.d.ts +117 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +234 -87
- package/dist/index.js.map +1 -1
- package/dist/jobs.d.ts +2 -41
- package/dist/jobs.d.ts.map +1 -1
- package/dist/jobs.js +23 -104
- package/dist/jobs.js.map +1 -1
- package/dist/log-stream-broker.d.ts +2 -2
- package/dist/log-stream-broker.d.ts.map +1 -1
- package/dist/log-stream-broker.js +3 -6
- package/dist/log-stream-broker.js.map +1 -1
- package/dist/manifest/atomicWrite.d.ts +16 -0
- package/dist/manifest/atomicWrite.d.ts.map +1 -0
- package/dist/manifest/atomicWrite.js +100 -0
- package/dist/manifest/atomicWrite.js.map +1 -0
- package/dist/manifest/resolver.d.ts +33 -0
- package/dist/manifest/resolver.d.ts.map +1 -0
- package/dist/manifest/resolver.js +52 -0
- package/dist/manifest/resolver.js.map +1 -0
- package/dist/manifest/schema.d.ts +53 -0
- package/dist/manifest/schema.d.ts.map +1 -0
- package/dist/manifest/schema.js +47 -0
- package/dist/manifest/schema.js.map +1 -0
- package/dist/manifest/store.d.ts +45 -0
- package/dist/manifest/store.d.ts.map +1 -0
- package/dist/manifest/store.js +220 -0
- package/dist/manifest/store.js.map +1 -0
- package/dist/resources.d.ts +1 -1
- package/dist/resources.d.ts.map +1 -1
- package/dist/resources.js +9 -19
- package/dist/resources.js.map +1 -1
- package/dist/runtime.d.ts +66 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +107 -26
- package/dist/runtime.js.map +1 -1
- package/dist/types.d.ts +187 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -2
- package/dist/updates/cache.d.ts +1 -1
- package/dist/updates/cache.d.ts.map +1 -1
- package/dist/updates/cache.js +8 -13
- package/dist/updates/cache.js.map +1 -1
- package/dist/updates/offline.js +1 -4
- package/dist/updates/offline.js.map +1 -1
- package/dist/updates/routes.d.ts +1 -1
- package/dist/updates/routes.d.ts.map +1 -1
- package/dist/updates/routes.js +1 -4
- package/dist/updates/routes.js.map +1 -1
- package/dist/updates/semver.js +1 -4
- package/dist/updates/semver.js.map +1 -1
- package/dist/updates/service.d.ts +5 -5
- package/dist/updates/service.d.ts.map +1 -1
- package/dist/updates/service.js +12 -17
- package/dist/updates/service.js.map +1 -1
- package/dist/updates/sources.d.ts +1 -1
- package/dist/updates/sources.d.ts.map +1 -1
- package/dist/updates/sources.js +2 -6
- package/dist/updates/sources.js.map +1 -1
- package/dist/updates/tagClassifier.d.ts +1 -1
- package/dist/updates/tagClassifier.d.ts.map +1 -1
- package/dist/updates/tagClassifier.js +1 -4
- package/dist/updates/tagClassifier.js.map +1 -1
- package/dist/updates/types.d.ts +1 -1
- package/dist/updates/types.d.ts.map +1 -1
- package/dist/updates/types.js +1 -2
- package/doc/plugin-developer-guide.md +114 -0
- package/package.json +12 -10
- package/public/chunks/_virtual_mf-localSharedImportMap___mfe_internal__signalk_container-BmrRsGF5.js +1 -0
- package/public/chunks/dist-oj8URvh5.js +4 -0
- package/public/chunks/hostInit-qNQhWMu1.js +1 -0
- package/public/chunks/preload-helper-CM6grPkN.js +1 -0
- package/public/chunks/virtualExposes-CjK-c_Lg.js +1 -0
- package/public/configpanel-entry.js +5 -0
- package/public/remoteEntry.js +1 -1
- package/public/520.js +0 -1
- package/public/540.js +0 -2
- package/public/540.js.LICENSE.txt +0 -9
- package/public/main.js +0 -1
- package/webpack.config.js +0 -51
package/dist/containers.js
CHANGED
|
@@ -1,83 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.MAX_TAIL = void 0;
|
|
37
|
-
exports.volumeArg = volumeArg;
|
|
38
|
-
exports.classifyVolumeSources = classifyVolumeSources;
|
|
39
|
-
exports.collectRecoveredVolumes = collectRecoveredVolumes;
|
|
40
|
-
exports.safeInvokeVolumeIssue = safeInvokeVolumeIssue;
|
|
41
|
-
exports.safeInvokeContainerLog = safeInvokeContainerLog;
|
|
42
|
-
exports.tailContainerLogs = tailContainerLogs;
|
|
43
|
-
exports.parsePositiveIntQuery = parsePositiveIntQuery;
|
|
44
|
-
exports.getContainerLogs = getContainerLogs;
|
|
45
|
-
exports.qualifyImage = qualifyImage;
|
|
46
|
-
exports.imageExists = imageExists;
|
|
47
|
-
exports.getImageDigest = getImageDigest;
|
|
48
|
-
exports.pullImage = pullImage;
|
|
49
|
-
exports.getContainerState = getContainerState;
|
|
50
|
-
exports.getLiveResources = getLiveResources;
|
|
51
|
-
exports.parsePortBindings = parsePortBindings;
|
|
52
|
-
exports.getActualPortBindings = getActualPortBindings;
|
|
53
|
-
exports.getLiveContainerConfig = getLiveContainerConfig;
|
|
54
|
-
exports.diffContainerConfig = diffContainerConfig;
|
|
55
|
-
exports.ensureRunning = ensureRunning;
|
|
56
|
-
exports.startContainer = startContainer;
|
|
57
|
-
exports.stopContainer = stopContainer;
|
|
58
|
-
exports.removeContainer = removeContainer;
|
|
59
|
-
exports.listContainers = listContainers;
|
|
60
|
-
exports.pruneImages = pruneImages;
|
|
61
|
-
exports.execInContainer = execInContainer;
|
|
62
|
-
exports.ensureNetwork = ensureNetwork;
|
|
63
|
-
exports.removeNetwork = removeNetwork;
|
|
64
|
-
exports.connectToNetwork = connectToNetwork;
|
|
65
|
-
exports.disconnectFromNetwork = disconnectFromNetwork;
|
|
66
|
-
exports.parseSelfContainerIdFromCgroup = parseSelfContainerIdFromCgroup;
|
|
67
|
-
exports.parseSelfContainerIdsFromCgroupFile = parseSelfContainerIdsFromCgroupFile;
|
|
68
|
-
exports.readSelfContainerIdsFromCgroup = readSelfContainerIdsFromCgroup;
|
|
69
|
-
exports.findSelfContainerId = findSelfContainerId;
|
|
70
|
-
exports.resolveSignalkDataSource = resolveSignalkDataSource;
|
|
71
|
-
exports.resolveHostPathFromMounts = resolveHostPathFromMounts;
|
|
72
|
-
exports.resolveHostPath = resolveHostPath;
|
|
73
|
-
exports.releaseReservedPort = releaseReservedPort;
|
|
74
|
-
exports.findAvailablePort = findAvailablePort;
|
|
75
|
-
exports.resolveSignalkNetworks = resolveSignalkNetworks;
|
|
76
|
-
exports.waitForReady = waitForReady;
|
|
77
|
-
const net = __importStar(require("net"));
|
|
78
|
-
const node_fs_1 = require("node:fs");
|
|
79
|
-
const runtime_1 = require("./runtime");
|
|
80
|
-
const resources_1 = require("./resources");
|
|
1
|
+
import * as net from "node:net";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { execRuntime, execRuntimeLong, isContainerized, spawnRuntimeStreaming, userMappingFlags, } from "./runtime.js";
|
|
4
|
+
import { resourceFlagsForRun } from "./resources.js";
|
|
81
5
|
const CONTAINER_PREFIX = "sk-";
|
|
82
6
|
function prefixedName(name) {
|
|
83
7
|
return name.startsWith(CONTAINER_PREFIX)
|
|
@@ -95,7 +19,7 @@ function prefixedName(name) {
|
|
|
95
19
|
* Used by both ContainerConfig.volumes (containers.ts) and JobConfig
|
|
96
20
|
* inputs/outputs (jobs.ts) so the named-volume guard stays in one place.
|
|
97
21
|
*/
|
|
98
|
-
function volumeArg(hostPath, containerPath, runtime, readOnly = false) {
|
|
22
|
+
export function volumeArg(hostPath, containerPath, runtime, readOnly = false) {
|
|
99
23
|
const isNamedVolume = !hostPath.startsWith("/") && !hostPath.startsWith(".");
|
|
100
24
|
const flags = [];
|
|
101
25
|
if (readOnly)
|
|
@@ -140,7 +64,7 @@ function volumeSource(raw) {
|
|
|
140
64
|
* `probe` is the host-path existence check; defaults to `existsSync`.
|
|
141
65
|
* Tests inject a stub so they don't touch the filesystem.
|
|
142
66
|
*/
|
|
143
|
-
function classifyVolumeSources(volumes, probe =
|
|
67
|
+
export function classifyVolumeSources(volumes, probe = existsSync) {
|
|
144
68
|
const kept = {};
|
|
145
69
|
const skipped = [];
|
|
146
70
|
const aborted = [];
|
|
@@ -195,7 +119,7 @@ function classifyVolumeSources(volumes, probe = node_fs_1.existsSync) {
|
|
|
195
119
|
*
|
|
196
120
|
* Pure function — no I/O.
|
|
197
121
|
*/
|
|
198
|
-
function collectRecoveredVolumes(prior, currentSkipped, currentAborted, kept) {
|
|
122
|
+
export function collectRecoveredVolumes(prior, currentSkipped, currentAborted, kept) {
|
|
199
123
|
if (!prior)
|
|
200
124
|
return [];
|
|
201
125
|
const stillMissing = new Set([...currentSkipped, ...currentAborted].map((v) => v.containerPath));
|
|
@@ -228,7 +152,7 @@ function collectRecoveredVolumes(prior, currentSkipped, currentAborted, kept) {
|
|
|
228
152
|
* Pure-by-design: `reportError` is injected so tests can capture
|
|
229
153
|
* the message instead of writing to `app.error`.
|
|
230
154
|
*/
|
|
231
|
-
function safeInvokeVolumeIssue(handler, event, reportError) {
|
|
155
|
+
export function safeInvokeVolumeIssue(handler, event, reportError) {
|
|
232
156
|
if (!handler)
|
|
233
157
|
return;
|
|
234
158
|
try {
|
|
@@ -249,7 +173,7 @@ function safeInvokeVolumeIssue(handler, event, reportError) {
|
|
|
249
173
|
* doc for why the `try { Promise.resolve(...).catch(...) }` dance
|
|
250
174
|
* is needed.
|
|
251
175
|
*/
|
|
252
|
-
function safeInvokeContainerLog(handler, line, reportError) {
|
|
176
|
+
export function safeInvokeContainerLog(handler, line, reportError) {
|
|
253
177
|
if (!handler)
|
|
254
178
|
return;
|
|
255
179
|
try {
|
|
@@ -271,8 +195,8 @@ function safeInvokeContainerLog(handler, line, reportError) {
|
|
|
271
195
|
*
|
|
272
196
|
* `spawn` is exposed for tests (defaults to `spawnRuntimeStreaming`).
|
|
273
197
|
*/
|
|
274
|
-
function tailContainerLogs(runtime, name, onLine, options) {
|
|
275
|
-
const spawnFn = options?.spawn ??
|
|
198
|
+
export function tailContainerLogs(runtime, name, onLine, options) {
|
|
199
|
+
const spawnFn = options?.spawn ?? spawnRuntimeStreaming;
|
|
276
200
|
const fullName = prefixedName(name);
|
|
277
201
|
const tail = String(normalizeTail(options?.startTail, 0));
|
|
278
202
|
return spawnFn(runtime, ["logs", "-f", "--tail", tail, fullName], onLine, {
|
|
@@ -289,7 +213,7 @@ function tailContainerLogs(runtime, name, onLine, options) {
|
|
|
289
213
|
});
|
|
290
214
|
}
|
|
291
215
|
/** Upper bound for `--tail N` accepted at the public boundary. */
|
|
292
|
-
|
|
216
|
+
export const MAX_TAIL = 10000;
|
|
293
217
|
/**
|
|
294
218
|
* Coerce an optional caller-supplied tail count to a finite,
|
|
295
219
|
* non-negative integer so a buggy or hostile caller can never
|
|
@@ -301,7 +225,7 @@ exports.MAX_TAIL = 10000;
|
|
|
301
225
|
function normalizeTail(value, fallback) {
|
|
302
226
|
if (value === undefined || !Number.isFinite(value))
|
|
303
227
|
return fallback;
|
|
304
|
-
return Math.min(Math.max(0, Math.floor(value)),
|
|
228
|
+
return Math.min(Math.max(0, Math.floor(value)), MAX_TAIL);
|
|
305
229
|
}
|
|
306
230
|
/**
|
|
307
231
|
* Validate an optional unsigned-integer query parameter (e.g.
|
|
@@ -314,7 +238,7 @@ function normalizeTail(value, fallback) {
|
|
|
314
238
|
* instead of forwarding silently-coerced values to runtime-facing
|
|
315
239
|
* logic. Exported so the parser is testable in isolation.
|
|
316
240
|
*/
|
|
317
|
-
function parsePositiveIntQuery(raw, field) {
|
|
241
|
+
export function parsePositiveIntQuery(raw, field) {
|
|
318
242
|
if (raw === undefined || raw === "")
|
|
319
243
|
return { value: undefined };
|
|
320
244
|
if (typeof raw !== "string")
|
|
@@ -356,7 +280,7 @@ function parsePositiveIntQuery(raw, field) {
|
|
|
356
280
|
* Used both by the `GET /containers/:name/logs` REST route and by
|
|
357
281
|
* `ContainerManagerApi.getLogs` for in-process consumer-plugin calls.
|
|
358
282
|
*/
|
|
359
|
-
async function getContainerLogs(runtime, name, options, exec =
|
|
283
|
+
export async function getContainerLogs(runtime, name, options, exec = execRuntime) {
|
|
360
284
|
const tail = normalizeTail(options?.tail, 200);
|
|
361
285
|
const args = ["logs", "--tail", String(tail)];
|
|
362
286
|
if (options?.since !== undefined &&
|
|
@@ -384,15 +308,31 @@ async function getContainerLogs(runtime, name, options, exec = runtime_1.execRun
|
|
|
384
308
|
};
|
|
385
309
|
return [...toLines(result.stdout), ...toLines(result.stderr)];
|
|
386
310
|
}
|
|
387
|
-
|
|
311
|
+
/**
|
|
312
|
+
* Bare image id: 64 hex chars, optionally with the `sha256:` prefix
|
|
313
|
+
* podman/docker print for `Config.Image`. Such ids must never be
|
|
314
|
+
* qualified with a registry — they're addressable by content, not by
|
|
315
|
+
* name.
|
|
316
|
+
*/
|
|
317
|
+
const BARE_IMAGE_ID = /^(?:sha256:)?[a-f0-9]{64}$/;
|
|
318
|
+
export function qualifyImage(image, runtime) {
|
|
388
319
|
// Podman requires fully qualified image names when unqualified-search
|
|
389
320
|
// registries are not configured. Prefix docker.io/ if missing.
|
|
390
321
|
if (runtime.runtime === "podman") {
|
|
391
|
-
|
|
322
|
+
// Bare image ids (e.g. `sha256:abc…` or `abc…`) are content-addressed
|
|
323
|
+
// and never need a registry prefix; pass through.
|
|
324
|
+
if (BARE_IMAGE_ID.test(image))
|
|
325
|
+
return image;
|
|
326
|
+
// Strip any `@sha256:...` digest before splitting — for an
|
|
327
|
+
// unqualified ref like `alpine@sha256:abc...` the digest's `:`
|
|
328
|
+
// would otherwise make us think the first segment is `host:port`.
|
|
329
|
+
const refWithoutDigest = image.split("@", 1)[0];
|
|
330
|
+
const parts = refWithoutDigest.split("/");
|
|
392
331
|
// Treat first component as a registry only if it has a dot, a colon
|
|
393
|
-
// (port), or is exactly "localhost".
|
|
332
|
+
// (port) appearing in a multi-segment ref, or is exactly "localhost".
|
|
333
|
+
// Otherwise, prefix docker.io/.
|
|
394
334
|
const looksLikeRegistry = parts[0].includes(".") ||
|
|
395
|
-
parts[0].includes(":") ||
|
|
335
|
+
(parts.length > 1 && parts[0].includes(":")) ||
|
|
396
336
|
parts[0] === "localhost";
|
|
397
337
|
if (parts.length <= 2 && !looksLikeRegistry) {
|
|
398
338
|
return `docker.io/${image}`;
|
|
@@ -400,7 +340,7 @@ function qualifyImage(image, runtime) {
|
|
|
400
340
|
}
|
|
401
341
|
return image;
|
|
402
342
|
}
|
|
403
|
-
async function imageExists(runtime, image, exec =
|
|
343
|
+
export async function imageExists(runtime, image, exec = execRuntime) {
|
|
404
344
|
const result = await exec(runtime, ["image", "inspect", image]);
|
|
405
345
|
return result.exitCode === 0;
|
|
406
346
|
}
|
|
@@ -413,10 +353,10 @@ async function imageExists(runtime, image, exec = runtime_1.execRuntime) {
|
|
|
413
353
|
* pulled image, or a container name to inspect the image a running
|
|
414
354
|
* container is using.
|
|
415
355
|
*/
|
|
416
|
-
async function getImageDigest(runtime, imageOrContainer) {
|
|
356
|
+
export async function getImageDigest(runtime, imageOrContainer, exec = execRuntime) {
|
|
417
357
|
// Try image inspect first; fall back to container inspect for names.
|
|
418
358
|
const qualified = qualifyImage(imageOrContainer, runtime);
|
|
419
|
-
const imgResult = await (
|
|
359
|
+
const imgResult = await exec(runtime, [
|
|
420
360
|
"image",
|
|
421
361
|
"inspect",
|
|
422
362
|
"--format",
|
|
@@ -427,7 +367,7 @@ async function getImageDigest(runtime, imageOrContainer) {
|
|
|
427
367
|
return imgResult.stdout.trim();
|
|
428
368
|
}
|
|
429
369
|
// Maybe it's a container name; .Image on a container returns the image ID.
|
|
430
|
-
const ctrResult = await (
|
|
370
|
+
const ctrResult = await exec(runtime, [
|
|
431
371
|
"inspect",
|
|
432
372
|
"--format",
|
|
433
373
|
"{{.Image}}",
|
|
@@ -438,13 +378,74 @@ async function getImageDigest(runtime, imageOrContainer) {
|
|
|
438
378
|
}
|
|
439
379
|
return null;
|
|
440
380
|
}
|
|
441
|
-
|
|
442
|
-
|
|
381
|
+
/**
|
|
382
|
+
* Return the first RepoDigest for an image reference, as
|
|
383
|
+
* `sha256:<hex>` (without the `image@` prefix). This is the
|
|
384
|
+
* *manifest* digest — what consumer plugins and CI tools speak —
|
|
385
|
+
* distinct from the local image ID returned by `getImageDigest`.
|
|
386
|
+
*
|
|
387
|
+
* Returns null when the image has no RepoDigests, which happens for:
|
|
388
|
+
* - locally-built images never pushed to a registry
|
|
389
|
+
* - images side-loaded via `podman load` from a tarball
|
|
390
|
+
*
|
|
391
|
+
* The resolver falls back to a `local:<image-id>` synthetic identity
|
|
392
|
+
* in that case so the manifest entry still round-trips.
|
|
393
|
+
*/
|
|
394
|
+
export async function getRepoDigest(runtime, image, exec = execRuntime) {
|
|
395
|
+
const qualified = qualifyImage(image, runtime);
|
|
396
|
+
// The {{if .RepoDigests}}{{end}} guard avoids `[<no value>]` output
|
|
397
|
+
// that some podman versions emit when RepoDigests is empty.
|
|
398
|
+
const result = await exec(runtime, [
|
|
399
|
+
"image",
|
|
400
|
+
"inspect",
|
|
401
|
+
"--format",
|
|
402
|
+
"{{if .RepoDigests}}{{index .RepoDigests 0}}{{end}}",
|
|
403
|
+
qualified,
|
|
404
|
+
]);
|
|
405
|
+
if (result.exitCode !== 0)
|
|
406
|
+
return null;
|
|
407
|
+
const raw = result.stdout.trim();
|
|
408
|
+
if (!raw)
|
|
409
|
+
return null;
|
|
410
|
+
const at = raw.lastIndexOf("@");
|
|
411
|
+
if (at < 0)
|
|
412
|
+
return null;
|
|
413
|
+
const digest = raw.slice(at + 1);
|
|
414
|
+
return /^sha256:[a-f0-9]{64}$/.test(digest) ? digest : null;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Return the digest of the image a *running* container is actually
|
|
418
|
+
* using, in the form `sha256:<hex>` or `local:<image-id>`.
|
|
419
|
+
*
|
|
420
|
+
* Distinct from "what `image:tag` resolves to right now": if someone
|
|
421
|
+
* `podman pull`'d the image after the container started, the local
|
|
422
|
+
* tag moved but the container is still on the old bits. This walks
|
|
423
|
+
* from the container's image-id (immutable for its lifetime) to its
|
|
424
|
+
* RepoDigests.
|
|
425
|
+
*
|
|
426
|
+
* Returns null if the container doesn't exist or the runtime can't
|
|
427
|
+
* read its image-id.
|
|
428
|
+
*/
|
|
429
|
+
export async function getLiveContainerDigest(runtime, containerName, exec = execRuntime) {
|
|
430
|
+
// The caller passes the unprefixed name from `ensureRunning`; the
|
|
431
|
+
// running container always carries the `sk-` prefix.
|
|
432
|
+
const imageId = await getImageDigest(runtime, prefixedName(containerName), exec);
|
|
433
|
+
if (!imageId)
|
|
434
|
+
return null;
|
|
435
|
+
const repoDigest = await getRepoDigest(runtime, imageId, exec);
|
|
436
|
+
if (repoDigest)
|
|
437
|
+
return repoDigest;
|
|
438
|
+
// Locally-built image with no RepoDigests — return the local id
|
|
439
|
+
// under the same `local:` namespace the resolver uses.
|
|
440
|
+
return `local:${imageId}`;
|
|
441
|
+
}
|
|
442
|
+
export async function pullImage(runtime, image, onProgress) {
|
|
443
|
+
const { exitCode, log } = await execRuntimeLong(runtime, ["pull", image], onProgress, 300000);
|
|
443
444
|
if (exitCode !== 0) {
|
|
444
445
|
throw new Error(`Failed to pull ${image}: ${log.slice(-5).join("\n")}`);
|
|
445
446
|
}
|
|
446
447
|
}
|
|
447
|
-
async function getContainerState(runtime, name, exec =
|
|
448
|
+
export async function getContainerState(runtime, name, exec = execRuntime) {
|
|
448
449
|
const fullName = prefixedName(name);
|
|
449
450
|
// Query multiple state fields and treat the container as running if
|
|
450
451
|
// ANY of them indicate running. Rationale: rootless podman on some
|
|
@@ -502,7 +503,7 @@ async function getContainerState(runtime, name, exec = runtime_1.execRuntime) {
|
|
|
502
503
|
*
|
|
503
504
|
* `exec` defaults to the production execRuntime; tests pass a stub.
|
|
504
505
|
*/
|
|
505
|
-
async function getLiveResources(runtime, name, exec =
|
|
506
|
+
export async function getLiveResources(runtime, name, exec = execRuntime) {
|
|
506
507
|
const fullName = prefixedName(name);
|
|
507
508
|
// Use Go-template format for reliable parsing across podman/docker.
|
|
508
509
|
// Each line is one numeric or string value; empty/zero means "unset".
|
|
@@ -599,7 +600,7 @@ function bytesToString(bytes) {
|
|
|
599
600
|
* already handle "no binding found" so a parse error degrades gracefully into
|
|
600
601
|
* "leave the existing cache alone".
|
|
601
602
|
*/
|
|
602
|
-
function parsePortBindings(json) {
|
|
603
|
+
export function parsePortBindings(json) {
|
|
603
604
|
const out = new Map();
|
|
604
605
|
if (!json || json === "null")
|
|
605
606
|
return out;
|
|
@@ -648,7 +649,7 @@ function parsePortBindings(json) {
|
|
|
648
649
|
* Returns an empty map on inspect failure; callers fall back to whatever they
|
|
649
650
|
* had pre-validation.
|
|
650
651
|
*/
|
|
651
|
-
async function getActualPortBindings(runtime, name, exec =
|
|
652
|
+
export async function getActualPortBindings(runtime, name, exec = execRuntime) {
|
|
652
653
|
const fullName = prefixedName(name);
|
|
653
654
|
const result = await exec(runtime, [
|
|
654
655
|
"inspect",
|
|
@@ -669,7 +670,7 @@ async function getActualPortBindings(runtime, name, exec = runtime_1.execRuntime
|
|
|
669
670
|
* what the container was actually created with, so a recreate can fire
|
|
670
671
|
* automatically instead of silently ignoring the new config until restart.
|
|
671
672
|
*/
|
|
672
|
-
async function getLiveContainerConfig(runtime, name, exec =
|
|
673
|
+
export async function getLiveContainerConfig(runtime, name, exec = execRuntime) {
|
|
673
674
|
const fullName = prefixedName(name);
|
|
674
675
|
// Sentinel between sections — `\x1f` (ASCII unit separator) avoids any
|
|
675
676
|
// collision with shell-meta or path characters that might appear inside
|
|
@@ -685,28 +686,40 @@ async function getLiveContainerConfig(runtime, name, exec = runtime_1.execRuntim
|
|
|
685
686
|
SEP +
|
|
686
687
|
"{{json .Config.Env}}" +
|
|
687
688
|
SEP +
|
|
688
|
-
"{{json .HostConfig.PortBindings}}"
|
|
689
|
+
"{{json .HostConfig.PortBindings}}" +
|
|
690
|
+
SEP +
|
|
691
|
+
"{{json .HostConfig.ExtraHosts}}" +
|
|
692
|
+
SEP +
|
|
693
|
+
"{{.Config.User}}";
|
|
689
694
|
const result = await exec(runtime, ["inspect", "--format", fmt, fullName]);
|
|
690
695
|
if (result.exitCode !== 0)
|
|
691
696
|
return null;
|
|
692
697
|
const parts = result.stdout.split(SEP);
|
|
693
|
-
if (parts.length !==
|
|
698
|
+
if (parts.length !== 8)
|
|
694
699
|
return null;
|
|
695
|
-
const [rawImage, rawCmd, rawNetworkMode, rawBinds, rawEnv, rawPortBindings] = parts;
|
|
696
|
-
// Split image into image+tag
|
|
697
|
-
//
|
|
698
|
-
//
|
|
700
|
+
const [rawImage, rawCmd, rawNetworkMode, rawBinds, rawEnv, rawPortBindings, rawExtraHosts, rawUser,] = parts;
|
|
701
|
+
// Split image into image+tag (and optional digest). Config.Image can
|
|
702
|
+
// be `repo:tag`, `repo@sha256:...`, or `repo:tag@sha256:...`.
|
|
703
|
+
// Registries with ports (`localhost:5000/foo:tag`) push the
|
|
704
|
+
// image-vs-tag colon past the last slash.
|
|
699
705
|
const imageRef = rawImage.trim();
|
|
700
|
-
|
|
701
|
-
|
|
706
|
+
let imageAndTag = imageRef;
|
|
707
|
+
let digest = null;
|
|
708
|
+
const atIdx = imageRef.indexOf("@sha256:");
|
|
709
|
+
if (atIdx >= 0) {
|
|
710
|
+
imageAndTag = imageRef.slice(0, atIdx);
|
|
711
|
+
digest = imageRef.slice(atIdx + 1);
|
|
712
|
+
}
|
|
713
|
+
const lastColon = imageAndTag.lastIndexOf(":");
|
|
714
|
+
const lastSlash = imageAndTag.lastIndexOf("/");
|
|
702
715
|
let image;
|
|
703
716
|
let tag;
|
|
704
717
|
if (lastColon > lastSlash) {
|
|
705
|
-
image =
|
|
706
|
-
tag =
|
|
718
|
+
image = imageAndTag.slice(0, lastColon);
|
|
719
|
+
tag = imageAndTag.slice(lastColon + 1);
|
|
707
720
|
}
|
|
708
721
|
else {
|
|
709
|
-
image =
|
|
722
|
+
image = imageAndTag;
|
|
710
723
|
tag = "latest";
|
|
711
724
|
}
|
|
712
725
|
let command = null;
|
|
@@ -770,7 +783,39 @@ async function getLiveContainerConfig(runtime, name, exec = runtime_1.execRuntim
|
|
|
770
783
|
// leave empty
|
|
771
784
|
}
|
|
772
785
|
const portBindings = parsePortBindingsFromJsonString(rawPortBindings);
|
|
773
|
-
|
|
786
|
+
const extraHosts = new Map();
|
|
787
|
+
try {
|
|
788
|
+
const parsed = JSON.parse(rawExtraHosts);
|
|
789
|
+
if (Array.isArray(parsed)) {
|
|
790
|
+
for (const entry of parsed) {
|
|
791
|
+
if (typeof entry !== "string")
|
|
792
|
+
continue;
|
|
793
|
+
// ExtraHosts format: "hostname:ipaddress"
|
|
794
|
+
const colon = entry.indexOf(":");
|
|
795
|
+
if (colon < 0)
|
|
796
|
+
continue;
|
|
797
|
+
const hostname = entry.slice(0, colon);
|
|
798
|
+
const ip = entry.slice(colon + 1);
|
|
799
|
+
extraHosts.set(hostname, ip);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
catch {
|
|
804
|
+
// leave empty
|
|
805
|
+
}
|
|
806
|
+
const user = (rawUser ?? "").trim();
|
|
807
|
+
return {
|
|
808
|
+
image,
|
|
809
|
+
tag,
|
|
810
|
+
digest,
|
|
811
|
+
command,
|
|
812
|
+
networkMode,
|
|
813
|
+
env,
|
|
814
|
+
binds,
|
|
815
|
+
portBindings,
|
|
816
|
+
extraHosts,
|
|
817
|
+
user,
|
|
818
|
+
};
|
|
774
819
|
}
|
|
775
820
|
/**
|
|
776
821
|
* Wrapper around `parsePortBindings` that keeps the container-port key as
|
|
@@ -897,10 +942,12 @@ function bindingsEqual(a, b) {
|
|
|
897
942
|
* - ports: container-port key compared as runtime-emitted (`9000/tcp`).
|
|
898
943
|
* Multiple host bindings per container port compared as a sorted set.
|
|
899
944
|
*/
|
|
900
|
-
function diffContainerConfig(requested, live, runtime, prior) {
|
|
945
|
+
export function diffContainerConfig(requested, live, runtime, prior) {
|
|
901
946
|
const drifted = [];
|
|
902
|
-
const requestedImageRef = qualifyImage(
|
|
903
|
-
|
|
947
|
+
const requestedImageRef = qualifyImage(requested.digest
|
|
948
|
+
? `${requested.image}@${requested.digest}`
|
|
949
|
+
: `${requested.image}:${requested.tag}`, runtime);
|
|
950
|
+
const liveImageRef = qualifyImage(live.digest ? `${live.image}@${live.digest}` : `${live.image}:${live.tag}`, runtime);
|
|
904
951
|
if (requestedImageRef !== liveImageRef)
|
|
905
952
|
drifted.push("image+tag");
|
|
906
953
|
if (requested.command !== undefined) {
|
|
@@ -989,11 +1036,55 @@ function diffContainerConfig(requested, live, runtime, prior) {
|
|
|
989
1036
|
}
|
|
990
1037
|
if (portsDrift)
|
|
991
1038
|
drifted.push("ports");
|
|
1039
|
+
// ExtraHosts: build canonical Map<hostname, ip> for each side.
|
|
1040
|
+
const requestedExtraHosts = new Map();
|
|
1041
|
+
if (requested.extraHosts) {
|
|
1042
|
+
for (const [hostname, ip] of Object.entries(requested.extraHosts)) {
|
|
1043
|
+
requestedExtraHosts.set(hostname, ip);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
// This plugin injects host.containers.internal:host-gateway in
|
|
1047
|
+
// buildRunArgs() for Docker so containers can reach the host the
|
|
1048
|
+
// same way Podman does natively. Mirror that here so the live
|
|
1049
|
+
// ExtraHosts (which records the flag) doesn't fire false drift —
|
|
1050
|
+
// but only when the user didn't supply their own override for the
|
|
1051
|
+
// same key.
|
|
1052
|
+
if (runtime.runtime === "docker" &&
|
|
1053
|
+
!requestedExtraHosts.has("host.containers.internal")) {
|
|
1054
|
+
requestedExtraHosts.set("host.containers.internal", "host-gateway");
|
|
1055
|
+
}
|
|
1056
|
+
let extraHostsDrift = requestedExtraHosts.size !== live.extraHosts.size;
|
|
1057
|
+
if (!extraHostsDrift) {
|
|
1058
|
+
for (const [hostname, ip] of requestedExtraHosts) {
|
|
1059
|
+
if (live.extraHosts.get(hostname) !== ip) {
|
|
1060
|
+
extraHostsDrift = true;
|
|
1061
|
+
break;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
if (extraHostsDrift)
|
|
1066
|
+
drifted.push("extraHosts");
|
|
1067
|
+
// User/ownership drift. Compute the `--user uid:gid` form the
|
|
1068
|
+
// translator would emit and compare to live `Config.User`. The
|
|
1069
|
+
// `--userns=keep-id` flag (rootless Podman) doesn't surface in
|
|
1070
|
+
// `Config.User`, so we only fire drift when the expected form is
|
|
1071
|
+
// a `--user` flag — that is, opt-out, rootless-Podman, and
|
|
1072
|
+
// unavailable-hostUser all suppress drift on this field by design.
|
|
1073
|
+
const expectedUserFlags = userMappingFlags(runtime, requested.user);
|
|
1074
|
+
const userIdx = expectedUserFlags.indexOf("--user");
|
|
1075
|
+
const expectedUser = userIdx >= 0 && userIdx + 1 < expectedUserFlags.length
|
|
1076
|
+
? expectedUserFlags[userIdx + 1]
|
|
1077
|
+
: null;
|
|
1078
|
+
if (expectedUser !== null && expectedUser !== live.user) {
|
|
1079
|
+
drifted.push("user");
|
|
1080
|
+
}
|
|
992
1081
|
return { drifted };
|
|
993
1082
|
}
|
|
994
1083
|
function buildRunArgs(name, config, runtime) {
|
|
995
1084
|
const fullName = prefixedName(name);
|
|
996
|
-
const imageRef = qualifyImage(
|
|
1085
|
+
const imageRef = qualifyImage(config.digest
|
|
1086
|
+
? `${config.image}@${config.digest}`
|
|
1087
|
+
: `${config.image}:${config.tag}`, runtime);
|
|
997
1088
|
const args = ["run", "-d", "--name", fullName];
|
|
998
1089
|
if (config.restart && config.restart !== "no") {
|
|
999
1090
|
args.push("--restart", config.restart);
|
|
@@ -1001,6 +1092,11 @@ function buildRunArgs(name, config, runtime) {
|
|
|
1001
1092
|
if (config.networkMode) {
|
|
1002
1093
|
args.push("--network", config.networkMode);
|
|
1003
1094
|
}
|
|
1095
|
+
// UID/GID alignment so files created inside the container on bind-
|
|
1096
|
+
// mounted host paths land owned by the host caller. Same decision
|
|
1097
|
+
// matrix as `runJob` (see `userMappingFlags`). `config.user === false`
|
|
1098
|
+
// opts out; the in-image UID/GID defaults to 0 when not declared.
|
|
1099
|
+
args.push(...userMappingFlags(runtime, config.user));
|
|
1004
1100
|
if (config.ports) {
|
|
1005
1101
|
for (const [containerPort, hostBind] of Object.entries(config.ports)) {
|
|
1006
1102
|
const port = containerPort.replace(/\/tcp$/, "");
|
|
@@ -1017,17 +1113,32 @@ function buildRunArgs(name, config, runtime) {
|
|
|
1017
1113
|
args.push("-e", `${key}=${value}`);
|
|
1018
1114
|
}
|
|
1019
1115
|
}
|
|
1116
|
+
// Add extra hosts: user-provided + (for Docker) the
|
|
1117
|
+
// host.containers.internal:host-gateway mapping Podman provides
|
|
1118
|
+
// natively. Skip the Docker injection if the user already supplied
|
|
1119
|
+
// their own value for the same key to avoid duplicate /etc/hosts
|
|
1120
|
+
// entries and the implicit first-match-wins override.
|
|
1121
|
+
const userHasInternalOverride = !!config.extraHosts &&
|
|
1122
|
+
Object.prototype.hasOwnProperty.call(config.extraHosts, "host.containers.internal");
|
|
1123
|
+
if (config.extraHosts) {
|
|
1124
|
+
for (const [hostname, ip] of Object.entries(config.extraHosts)) {
|
|
1125
|
+
args.push("--add-host", `${hostname}:${ip}`);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
if (runtime.runtime === "docker" && !userHasInternalOverride) {
|
|
1129
|
+
args.push("--add-host", "host.containers.internal:host-gateway");
|
|
1130
|
+
}
|
|
1020
1131
|
// Resource limits (--cpus, --memory, --pids-limit, etc.)
|
|
1021
1132
|
// Fields whose backing cgroup controller is unavailable on this
|
|
1022
1133
|
// runtime are silently dropped.
|
|
1023
|
-
args.push(...
|
|
1134
|
+
args.push(...resourceFlagsForRun(config.resources, runtime));
|
|
1024
1135
|
args.push(imageRef);
|
|
1025
1136
|
if (config.command) {
|
|
1026
1137
|
args.push(...config.command);
|
|
1027
1138
|
}
|
|
1028
1139
|
return args;
|
|
1029
1140
|
}
|
|
1030
|
-
async function ensureRunning(runtime, name, config, debug, options, exec =
|
|
1141
|
+
export async function ensureRunning(runtime, name, config, debug, options, exec = execRuntime,
|
|
1031
1142
|
/**
|
|
1032
1143
|
* Prior `ContainerConfig` from the previous `ensureRunning` call within
|
|
1033
1144
|
* this signalk-container lifetime, if any. Used to detect "unset" drift
|
|
@@ -1039,7 +1150,9 @@ async function ensureRunning(runtime, name, config, debug, options, exec = runti
|
|
|
1039
1150
|
prior, _postRecreate = false) {
|
|
1040
1151
|
const state = await getContainerState(runtime, name, exec);
|
|
1041
1152
|
const fullName = prefixedName(name);
|
|
1042
|
-
const imageRef = qualifyImage(
|
|
1153
|
+
const imageRef = qualifyImage(config.digest
|
|
1154
|
+
? `${config.image}@${config.digest}`
|
|
1155
|
+
: `${config.image}:${config.tag}`, runtime);
|
|
1043
1156
|
// Drift detection is needed for both "running" and "stopped" — a stopped
|
|
1044
1157
|
// container with stale env/volumes/ports/command would otherwise be
|
|
1045
1158
|
// re-started with the OLD config. Inspect works on both states.
|
|
@@ -1106,14 +1219,14 @@ prior, _postRecreate = false) {
|
|
|
1106
1219
|
}
|
|
1107
1220
|
}
|
|
1108
1221
|
}
|
|
1109
|
-
async function startContainer(runtime, name) {
|
|
1222
|
+
export async function startContainer(runtime, name) {
|
|
1110
1223
|
const fullName = prefixedName(name);
|
|
1111
|
-
const result = await
|
|
1224
|
+
const result = await execRuntime(runtime, ["start", fullName]);
|
|
1112
1225
|
if (result.exitCode !== 0) {
|
|
1113
1226
|
throw new Error(`Failed to start ${fullName}: ${result.stderr}`);
|
|
1114
1227
|
}
|
|
1115
1228
|
}
|
|
1116
|
-
async function fixVolumePermissions(runtime, name, exec =
|
|
1229
|
+
async function fixVolumePermissions(runtime, name, exec = execRuntime) {
|
|
1117
1230
|
const fullName = prefixedName(name);
|
|
1118
1231
|
const state = await getContainerState(runtime, name, exec);
|
|
1119
1232
|
if (state !== "running")
|
|
@@ -1134,10 +1247,10 @@ async function fixVolumePermissions(runtime, name, exec = runtime_1.execRuntime)
|
|
|
1134
1247
|
// back silently if chmod isn't available in the image (distroless etc.).
|
|
1135
1248
|
await exec(runtime, ["exec", fullName, "chmod", "-R", "o+rwX", ...mounts]);
|
|
1136
1249
|
}
|
|
1137
|
-
async function stopContainer(runtime, name) {
|
|
1250
|
+
export async function stopContainer(runtime, name) {
|
|
1138
1251
|
const fullName = prefixedName(name);
|
|
1139
1252
|
await fixVolumePermissions(runtime, name).catch(() => { });
|
|
1140
|
-
const result = await
|
|
1253
|
+
const result = await execRuntime(runtime, ["stop", fullName]);
|
|
1141
1254
|
if (result.exitCode !== 0) {
|
|
1142
1255
|
const state = await getContainerState(runtime, name);
|
|
1143
1256
|
if (state !== "stopped" && state !== "missing") {
|
|
@@ -1145,7 +1258,7 @@ async function stopContainer(runtime, name) {
|
|
|
1145
1258
|
}
|
|
1146
1259
|
}
|
|
1147
1260
|
}
|
|
1148
|
-
async function removeContainer(runtime, name, exec =
|
|
1261
|
+
export async function removeContainer(runtime, name, exec = execRuntime) {
|
|
1149
1262
|
const fullName = prefixedName(name);
|
|
1150
1263
|
await fixVolumePermissions(runtime, name, exec).catch(() => { });
|
|
1151
1264
|
await exec(runtime, ["stop", fullName]);
|
|
@@ -1154,8 +1267,8 @@ async function removeContainer(runtime, name, exec = runtime_1.execRuntime) {
|
|
|
1154
1267
|
throw new Error(`Failed to remove ${fullName}: ${result.stderr}`);
|
|
1155
1268
|
}
|
|
1156
1269
|
}
|
|
1157
|
-
async function listContainers(runtime) {
|
|
1158
|
-
const result = await
|
|
1270
|
+
export async function listContainers(runtime) {
|
|
1271
|
+
const result = await execRuntime(runtime, [
|
|
1159
1272
|
"ps",
|
|
1160
1273
|
"-a",
|
|
1161
1274
|
"--filter",
|
|
@@ -1183,8 +1296,8 @@ async function listContainers(runtime) {
|
|
|
1183
1296
|
};
|
|
1184
1297
|
});
|
|
1185
1298
|
}
|
|
1186
|
-
async function pruneImages(runtime) {
|
|
1187
|
-
const result = await
|
|
1299
|
+
export async function pruneImages(runtime) {
|
|
1300
|
+
const result = await execRuntime(runtime, ["image", "prune", "-f"]);
|
|
1188
1301
|
if (result.exitCode !== 0) {
|
|
1189
1302
|
throw new Error(`Prune failed: ${result.stderr}`);
|
|
1190
1303
|
}
|
|
@@ -1195,28 +1308,28 @@ async function pruneImages(runtime) {
|
|
|
1195
1308
|
spaceReclaimed: reclaimedMatch?.[1] ?? "0 B",
|
|
1196
1309
|
};
|
|
1197
1310
|
}
|
|
1198
|
-
async function execInContainer(runtime, name, command) {
|
|
1311
|
+
export async function execInContainer(runtime, name, command) {
|
|
1199
1312
|
const fullName = prefixedName(name);
|
|
1200
|
-
return
|
|
1313
|
+
return execRuntime(runtime, ["exec", fullName, ...command]);
|
|
1201
1314
|
}
|
|
1202
|
-
async function ensureNetwork(runtime, name) {
|
|
1203
|
-
const inspect = await
|
|
1315
|
+
export async function ensureNetwork(runtime, name) {
|
|
1316
|
+
const inspect = await execRuntime(runtime, ["network", "inspect", name]);
|
|
1204
1317
|
if (inspect.exitCode !== 0) {
|
|
1205
|
-
const create = await
|
|
1318
|
+
const create = await execRuntime(runtime, ["network", "create", name]);
|
|
1206
1319
|
if (create.exitCode !== 0 && !create.stderr.includes("already exists")) {
|
|
1207
1320
|
throw new Error(`Failed to create network ${name}: ${create.stderr}`);
|
|
1208
1321
|
}
|
|
1209
1322
|
}
|
|
1210
1323
|
}
|
|
1211
|
-
async function removeNetwork(runtime, name) {
|
|
1212
|
-
const result = await
|
|
1324
|
+
export async function removeNetwork(runtime, name) {
|
|
1325
|
+
const result = await execRuntime(runtime, ["network", "rm", name]);
|
|
1213
1326
|
if (result.exitCode !== 0 && !result.stderr.includes("not found")) {
|
|
1214
1327
|
throw new Error(`Failed to remove network ${name}: ${result.stderr}`);
|
|
1215
1328
|
}
|
|
1216
1329
|
}
|
|
1217
|
-
async function connectToNetwork(runtime, containerName, networkName) {
|
|
1330
|
+
export async function connectToNetwork(runtime, containerName, networkName) {
|
|
1218
1331
|
const fullName = prefixedName(containerName);
|
|
1219
|
-
const result = await
|
|
1332
|
+
const result = await execRuntime(runtime, [
|
|
1220
1333
|
"network",
|
|
1221
1334
|
"connect",
|
|
1222
1335
|
networkName,
|
|
@@ -1230,9 +1343,9 @@ async function connectToNetwork(runtime, containerName, networkName) {
|
|
|
1230
1343
|
throw new Error(`Failed to connect ${fullName} to ${networkName}: ${result.stderr}`);
|
|
1231
1344
|
}
|
|
1232
1345
|
}
|
|
1233
|
-
async function disconnectFromNetwork(runtime, containerName, networkName) {
|
|
1346
|
+
export async function disconnectFromNetwork(runtime, containerName, networkName) {
|
|
1234
1347
|
const fullName = prefixedName(containerName);
|
|
1235
|
-
const result = await
|
|
1348
|
+
const result = await execRuntime(runtime, [
|
|
1236
1349
|
"network",
|
|
1237
1350
|
"disconnect",
|
|
1238
1351
|
networkName,
|
|
@@ -1266,7 +1379,7 @@ async function disconnectFromNetwork(runtime, containerName, networkName) {
|
|
|
1266
1379
|
* 12+ hex char run; `docker inspect` will reject false positives
|
|
1267
1380
|
* (e.g. systemd slice names that happen to be hex).
|
|
1268
1381
|
*/
|
|
1269
|
-
function parseSelfContainerIdFromCgroup(line) {
|
|
1382
|
+
export function parseSelfContainerIdFromCgroup(line) {
|
|
1270
1383
|
// 1) cgroup v1 path: `/<runtime>/<id>` where runtime is `docker`,
|
|
1271
1384
|
// `kubepods/...`, etc.
|
|
1272
1385
|
const v1 = line.match(/[/]([0-9a-f]{12,64})(?:[/.]|$)/i);
|
|
@@ -1287,7 +1400,7 @@ function parseSelfContainerIdFromCgroup(line) {
|
|
|
1287
1400
|
* tests can drive the multi-line / dedup logic without touching
|
|
1288
1401
|
* `/proc/self/cgroup` (which varies across test hosts).
|
|
1289
1402
|
*/
|
|
1290
|
-
function parseSelfContainerIdsFromCgroupFile(content) {
|
|
1403
|
+
export function parseSelfContainerIdsFromCgroupFile(content) {
|
|
1291
1404
|
const seen = new Set();
|
|
1292
1405
|
const ids = [];
|
|
1293
1406
|
for (const line of content.split("\n")) {
|
|
@@ -1316,10 +1429,10 @@ function parseSelfContainerIdsFromCgroupFile(content) {
|
|
|
1316
1429
|
*
|
|
1317
1430
|
* Skipped in production paths when `isContainerized()` is false.
|
|
1318
1431
|
*/
|
|
1319
|
-
function readSelfContainerIdsFromCgroup() {
|
|
1432
|
+
export function readSelfContainerIdsFromCgroup() {
|
|
1320
1433
|
let content;
|
|
1321
1434
|
try {
|
|
1322
|
-
content =
|
|
1435
|
+
content = readFileSync("/proc/self/cgroup", "utf8");
|
|
1323
1436
|
}
|
|
1324
1437
|
catch {
|
|
1325
1438
|
return [];
|
|
@@ -1337,7 +1450,7 @@ function readSelfContainerIdsFromCgroup() {
|
|
|
1337
1450
|
async function tryInspect(runtime, candidateId, debug, source) {
|
|
1338
1451
|
if (!candidateId)
|
|
1339
1452
|
return null;
|
|
1340
|
-
const result = await
|
|
1453
|
+
const result = await execRuntime(runtime, [
|
|
1341
1454
|
"inspect",
|
|
1342
1455
|
"--format",
|
|
1343
1456
|
"{{.Id}}",
|
|
@@ -1371,7 +1484,7 @@ async function tryInspect(runtime, candidateId, debug, source) {
|
|
|
1371
1484
|
* unset" behaviour — fall back to bare-metal-style handling, or
|
|
1372
1485
|
* return null to the caller (depends on the consumer).
|
|
1373
1486
|
*/
|
|
1374
|
-
async function findSelfContainerId(runtime, debug = () => { }) {
|
|
1487
|
+
export async function findSelfContainerId(runtime, debug = () => { }) {
|
|
1375
1488
|
// 1. Explicit override — no `inspect` validation needed since the
|
|
1376
1489
|
// operator chose this deliberately. An invalid value here
|
|
1377
1490
|
// surfaces as the same downstream error the user would have
|
|
@@ -1413,8 +1526,8 @@ async function findSelfContainerId(runtime, debug = () => { }) {
|
|
|
1413
1526
|
* a ContainerConfig. The content visible at mountPoint inside the managed
|
|
1414
1527
|
* container will always correspond to the root of dataDir.
|
|
1415
1528
|
*/
|
|
1416
|
-
async function resolveSignalkDataSource(dataDir, runtime, debug = () => { }) {
|
|
1417
|
-
if (!
|
|
1529
|
+
export async function resolveSignalkDataSource(dataDir, runtime, debug = () => { }) {
|
|
1530
|
+
if (!isContainerized()) {
|
|
1418
1531
|
// Running bare-metal: dataDir is already a host filesystem path.
|
|
1419
1532
|
return dataDir;
|
|
1420
1533
|
}
|
|
@@ -1427,7 +1540,7 @@ async function resolveSignalkDataSource(dataDir, runtime, debug = () => { }) {
|
|
|
1427
1540
|
debug(`resolveSignalkDataSource: could not detect self container id; falling back to dataDir=${dataDir}`);
|
|
1428
1541
|
return dataDir;
|
|
1429
1542
|
}
|
|
1430
|
-
const result = await
|
|
1543
|
+
const result = await execRuntime(runtime, [
|
|
1431
1544
|
"inspect",
|
|
1432
1545
|
"--format",
|
|
1433
1546
|
"{{range .Mounts}}{{.Type}}|{{.Name}}|{{.Source}}|{{.Destination}}\n{{end}}",
|
|
@@ -1477,7 +1590,7 @@ async function resolveSignalkDataSource(dataDir, runtime, debug = () => { }) {
|
|
|
1477
1590
|
* logic can be unit-tested independently of the runtime. Returns null
|
|
1478
1591
|
* when no mount covers `absPath`.
|
|
1479
1592
|
*/
|
|
1480
|
-
function resolveHostPathFromMounts(absPath, mounts) {
|
|
1593
|
+
export function resolveHostPathFromMounts(absPath, mounts) {
|
|
1481
1594
|
// Longest-prefix match. Same logic as resolveSignalkDataSource — handles
|
|
1482
1595
|
// exact matches and parent-directory mounts uniformly.
|
|
1483
1596
|
let best = null;
|
|
@@ -1531,8 +1644,8 @@ function resolveHostPathFromMounts(absPath, mounts) {
|
|
|
1531
1644
|
* should surface an actionable error (not silently fall through, as
|
|
1532
1645
|
* `resolveSignalkDataSource` does for backwards-compat).
|
|
1533
1646
|
*/
|
|
1534
|
-
async function resolveHostPath(absPath, runtime, debug = () => { }) {
|
|
1535
|
-
if (!
|
|
1647
|
+
export async function resolveHostPath(absPath, runtime, debug = () => { }) {
|
|
1648
|
+
if (!isContainerized()) {
|
|
1536
1649
|
// Bare-metal: the absolute path IS the host path. No subpath needed.
|
|
1537
1650
|
return { source: absPath, subPath: "" };
|
|
1538
1651
|
}
|
|
@@ -1544,7 +1657,7 @@ async function resolveHostPath(absPath, runtime, debug = () => { }) {
|
|
|
1544
1657
|
debug(`resolveHostPath: could not detect self container id; cannot resolve ${absPath}`);
|
|
1545
1658
|
return null;
|
|
1546
1659
|
}
|
|
1547
|
-
const result = await
|
|
1660
|
+
const result = await execRuntime(runtime, [
|
|
1548
1661
|
"inspect",
|
|
1549
1662
|
"--format",
|
|
1550
1663
|
"{{range .Mounts}}{{.Type}}|{{.Name}}|{{.Source}}|{{.Destination}}\n{{end}}",
|
|
@@ -1584,7 +1697,7 @@ const reservedPorts = new Set();
|
|
|
1584
1697
|
* (so the OS-level bind now prevents collisions), or when the container
|
|
1585
1698
|
* creation failed (so the next attempt can re-probe freely).
|
|
1586
1699
|
*/
|
|
1587
|
-
function releaseReservedPort(port) {
|
|
1700
|
+
export function releaseReservedPort(port) {
|
|
1588
1701
|
reservedPorts.delete(port);
|
|
1589
1702
|
}
|
|
1590
1703
|
/**
|
|
@@ -1614,7 +1727,7 @@ function isPortAvailable(port) {
|
|
|
1614
1727
|
* Used by the `signalkAccessiblePorts` bare-metal path to prefer the
|
|
1615
1728
|
* declared port number while gracefully stepping over conflicts.
|
|
1616
1729
|
*/
|
|
1617
|
-
async function findAvailablePort(preferred) {
|
|
1730
|
+
export async function findAvailablePort(preferred) {
|
|
1618
1731
|
for (let port = preferred; port <= 65535; port++) {
|
|
1619
1732
|
if (reservedPorts.has(port))
|
|
1620
1733
|
continue;
|
|
@@ -1645,8 +1758,8 @@ async function findAvailablePort(preferred) {
|
|
|
1645
1758
|
* back to `networkMode: container:<self-container-id>`. A non-empty
|
|
1646
1759
|
* array contains the user-defined network names to attach to.
|
|
1647
1760
|
*/
|
|
1648
|
-
async function resolveSignalkNetworks(runtime, debug = () => { }) {
|
|
1649
|
-
if (!
|
|
1761
|
+
export async function resolveSignalkNetworks(runtime, debug = () => { }) {
|
|
1762
|
+
if (!isContainerized())
|
|
1650
1763
|
return null;
|
|
1651
1764
|
// Cascade detection — see `findSelfContainerId`. Critically this fixes
|
|
1652
1765
|
// `network_mode: host` deployments where HOSTNAME is the host machine
|
|
@@ -1656,7 +1769,7 @@ async function resolveSignalkNetworks(runtime, debug = () => { }) {
|
|
|
1656
1769
|
debug("resolveSignalkNetworks: could not detect self container id, returning null");
|
|
1657
1770
|
return null;
|
|
1658
1771
|
}
|
|
1659
|
-
const result = await
|
|
1772
|
+
const result = await execRuntime(runtime, [
|
|
1660
1773
|
"inspect",
|
|
1661
1774
|
"--format",
|
|
1662
1775
|
"{{range $k,$v := .NetworkSettings.Networks}}{{$k}}\n{{end}}",
|
|
@@ -1673,7 +1786,7 @@ async function resolveSignalkNetworks(runtime, debug = () => { }) {
|
|
|
1673
1786
|
debug(`resolveSignalkNetworks: all=${all.join(",")} userDefined=${userDefined.join(",")}`);
|
|
1674
1787
|
return userDefined;
|
|
1675
1788
|
}
|
|
1676
|
-
async function waitForReady(url, timeoutMs = 30000, intervalMs = 500) {
|
|
1789
|
+
export async function waitForReady(url, timeoutMs = 30000, intervalMs = 500) {
|
|
1677
1790
|
const deadline = Date.now() + timeoutMs;
|
|
1678
1791
|
while (Date.now() < deadline) {
|
|
1679
1792
|
try {
|