signalk-container 1.35.0 → 1.35.2
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/.release-please-manifest.json +1 -1
- package/AGENTS.md +4 -2
- package/dist/containers.d.ts +28 -1
- package/dist/containers.d.ts.map +1 -1
- package/dist/containers.js +65 -16
- package/dist/containers.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +82 -53
- package/dist/index.js.map +1 -1
- package/dist/resources.d.ts +32 -0
- package/dist/resources.d.ts.map +1 -1
- package/dist/resources.js +80 -0
- package/dist/resources.js.map +1 -1
- package/dist/types.d.ts +20 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { existsSync, promises as fsp } from "node:fs";
|
|
3
|
-
import { clampCpusToHost, fieldsRequiringRecreateForUnset, filterUnsupportedLimits, mergeResourceLimits, minimizeOverride, resourceLimitsEqual, tryLiveUpdate, } from "./resources.js";
|
|
3
|
+
import { clampCpusToHost, fieldsRequiringRecreateForUnset, filterUnsupportedLimits, mergeResourceLimits, minimizeOverride, resourceLimitsEqual, tryLiveUpdate, withoutDefaultMemorySwap, withoutInjectedResources, } from "./resources.js";
|
|
4
4
|
import { detectRuntime, isContainerized, setDisableUserns } from "./runtime.js";
|
|
5
5
|
import { setNamespace } from "./namespace.js";
|
|
6
6
|
import { makeDegradationEmitter, } from "./notifications.js";
|
|
@@ -1443,7 +1443,38 @@ export default (app) => {
|
|
|
1443
1443
|
// detection in ensureRunning recreates the container with
|
|
1444
1444
|
// filteredMerged already applied) does not trigger a spurious
|
|
1445
1445
|
// tryLiveUpdate against an already-correct container.
|
|
1446
|
-
|
|
1446
|
+
//
|
|
1447
|
+
// Provenance ladder — what was actually *requested*, so a field
|
|
1448
|
+
// present in the live limits that never appeared in any request is
|
|
1449
|
+
// recognized as a runtime artifact, not a user unset (notably the
|
|
1450
|
+
// `oom_score_adj` rootless Podman clamps onto a child container
|
|
1451
|
+
// when signalk-server's own oom_score_adj is non-zero):
|
|
1452
|
+
//
|
|
1453
|
+
// 1. Warm cache: the PRIOR ensureRunning's post-filter
|
|
1454
|
+
// `filteredMerged` (cached in `lastConfigs`, read into
|
|
1455
|
+
// `priorConfig` above before this call overwrote it). Using
|
|
1456
|
+
// the prior request (not the current `filteredMerged`) still
|
|
1457
|
+
// flags a genuine unset: when a user removes a `memory` cap
|
|
1458
|
+
// from `containerOverrides`, the field was in the prior
|
|
1459
|
+
// request and the recreate warning fires.
|
|
1460
|
+
// 2. Cold cache (fresh server process): the requested-resources
|
|
1461
|
+
// label stamped on the container at create time — durable
|
|
1462
|
+
// provenance that survives restarts.
|
|
1463
|
+
// 3. Neither (container predates the label): `undefined`, NOT
|
|
1464
|
+
// `{}` — an empty object would assert "nothing was ever
|
|
1465
|
+
// requested" and silently suppress a real stale
|
|
1466
|
+
// `memory`/`memorySwap` cap the current config removes.
|
|
1467
|
+
// `undefined` falls back to the plain current-vs-live check
|
|
1468
|
+
// minus runtime-injected fields, so a genuine memory unset
|
|
1469
|
+
// is still surfaced and an inherited oom_score_adj is not
|
|
1470
|
+
// treated as an unset.
|
|
1471
|
+
//
|
|
1472
|
+
// The runtime-injected fields nobody requested are left out of
|
|
1473
|
+
// `postLimits` altogether, so an inherited oom_score_adj alone does
|
|
1474
|
+
// not count as a difference and send a live update on every start.
|
|
1475
|
+
const createdWith = await getRequestedResources(name);
|
|
1476
|
+
const priorRequested = priorConfig?.resources ?? createdWith;
|
|
1477
|
+
const postLimits = withoutInjectedResources(withoutDefaultMemorySwap(await getLiveResources(runtimeInfo, name), filteredMerged, createdWith), filteredMerged, priorRequested);
|
|
1447
1478
|
if (!resourceLimitsEqual(postLimits, filteredMerged)) {
|
|
1448
1479
|
const fullName = prefixedName(name);
|
|
1449
1480
|
// Bug E: if any field is being UNSET and it can't be unset
|
|
@@ -1452,32 +1483,6 @@ export default (app) => {
|
|
|
1452
1483
|
// ensureRunning's "already running" branch — that would
|
|
1453
1484
|
// surprise the consumer plugin. Instead, log a clear warning
|
|
1454
1485
|
// pointing the user to the explicit recreate path.
|
|
1455
|
-
//
|
|
1456
|
-
// Provenance ladder — what was actually *requested*, so a field
|
|
1457
|
-
// present in `postLimits` that never appeared in any request is
|
|
1458
|
-
// recognized as a runtime artifact, not a user unset (notably the
|
|
1459
|
-
// `oom_score_adj` rootless Podman clamps onto a child container
|
|
1460
|
-
// when signalk-server's own oom_score_adj is non-zero):
|
|
1461
|
-
//
|
|
1462
|
-
// 1. Warm cache: the PRIOR ensureRunning's post-filter
|
|
1463
|
-
// `filteredMerged` (cached in `lastConfigs`, read into
|
|
1464
|
-
// `priorConfig` above before this call overwrote it). Using
|
|
1465
|
-
// the prior request (not the current `filteredMerged`) still
|
|
1466
|
-
// flags a genuine unset: when a user removes a `memory` cap
|
|
1467
|
-
// from `containerOverrides`, the field was in the prior
|
|
1468
|
-
// request and the recreate warning fires.
|
|
1469
|
-
// 2. Cold cache (fresh server process): the requested-resources
|
|
1470
|
-
// label stamped on the container at create time — durable
|
|
1471
|
-
// provenance that survives restarts.
|
|
1472
|
-
// 3. Neither (container predates the label): `undefined`, NOT
|
|
1473
|
-
// `{}` — an empty object would assert "nothing was ever
|
|
1474
|
-
// requested" and silently suppress a real stale
|
|
1475
|
-
// `memory`/`memorySwap` cap the current config removes.
|
|
1476
|
-
// `undefined` falls back to the plain current-vs-live check
|
|
1477
|
-
// minus runtime-injected fields, so a genuine memory unset
|
|
1478
|
-
// is still surfaced while the inherited oom_score_adj no
|
|
1479
|
-
// longer warns on every server start (#216).
|
|
1480
|
-
const priorRequested = priorConfig?.resources ?? (await getRequestedResources(name));
|
|
1481
1486
|
const cannotUnset = fieldsRequiringRecreateForUnset(postLimits, filteredMerged, priorRequested);
|
|
1482
1487
|
if (cannotUnset.length > 0) {
|
|
1483
1488
|
app.error(`ensureRunning(${name}): cannot live-unset fields ${cannotUnset.join(", ")} on already-running container. ` +
|
|
@@ -1485,12 +1490,6 @@ export default (app) => {
|
|
|
1485
1490
|
`Use POST /plugins/signalk-container/api/containers/${name}/resources to force a recreate.`);
|
|
1486
1491
|
// Still try to apply the OTHER (settable) fields via live update.
|
|
1487
1492
|
}
|
|
1488
|
-
else if (priorRequested === undefined &&
|
|
1489
|
-
postLimits.oomScoreAdj != null &&
|
|
1490
|
-
filteredMerged.oomScoreAdj == null) {
|
|
1491
|
-
app.debug(`ensureRunning(${name}): ignoring live oomScoreAdj=${postLimits.oomScoreAdj} with no provenance — ` +
|
|
1492
|
-
`runtime-injected (rootless podman inherits the server's oom_score_adj), not a consumer request`);
|
|
1493
|
-
}
|
|
1494
1493
|
const live = await tryLiveUpdate(runtimeInfo, fullName, filteredMerged, restartPolicyFor(effectiveConfig));
|
|
1495
1494
|
if (!live.ok) {
|
|
1496
1495
|
// Live update failed (e.g. cpuset on a host that doesn't
|
|
@@ -1609,15 +1608,21 @@ export default (app) => {
|
|
|
1609
1608
|
if (!runtimeInfo)
|
|
1610
1609
|
throw new Error("No container runtime available");
|
|
1611
1610
|
// Always attempt remove. `removeContainer` is idempotent on truly-
|
|
1612
|
-
// missing containers (stop
|
|
1613
|
-
// and
|
|
1614
|
-
//
|
|
1615
|
-
//
|
|
1616
|
-
//
|
|
1617
|
-
//
|
|
1618
|
-
//
|
|
1611
|
+
// missing containers (a failed stop of a container that is gone is
|
|
1612
|
+
// tolerated, and `rm -f` treats not-found as success), and
|
|
1613
|
+
// afterContainerRemoved's cache evictions / broker teardown are
|
|
1614
|
+
// no-ops on a name nobody registered. No need to precondition on
|
|
1615
|
+
// getState — that would only add a TOCTOU window before the actual
|
|
1616
|
+
// remove. If the runtime does report a real failure, re-check state:
|
|
1617
|
+
// a benign disappearance (someone else removed it concurrently) is
|
|
1618
|
+
// acceptable; anything else propagates, including a graceful stop
|
|
1619
|
+
// the runtime refused while the container kept running. The stop is
|
|
1620
|
+
// graceful because the new container takes over the old one's data.
|
|
1619
1621
|
try {
|
|
1620
|
-
await
|
|
1622
|
+
await removeContainer(runtimeInfo, name, undefined, {
|
|
1623
|
+
graceful: true,
|
|
1624
|
+
});
|
|
1625
|
+
afterContainerRemoved(name);
|
|
1621
1626
|
}
|
|
1622
1627
|
catch (err) {
|
|
1623
1628
|
const now = await getContainerState(runtimeInfo, name);
|
|
@@ -1921,7 +1926,16 @@ export default (app) => {
|
|
|
1921
1926
|
// the user's intent but the container has the old value
|
|
1922
1927
|
// - manual `podman update` from outside Signal K isn't tracked
|
|
1923
1928
|
// Always compare against truth.
|
|
1924
|
-
|
|
1929
|
+
//
|
|
1930
|
+
// Provenance guard: only force a recreate for a field the consumer
|
|
1931
|
+
// actually requested before this update (plugin default ⊕ the
|
|
1932
|
+
// stored override). A field that shows up live purely because the
|
|
1933
|
+
// runtime injected it (a rootless-Podman-inherited oom_score_adj)
|
|
1934
|
+
// was never the user's to unset, so it is left out of `liveBefore`:
|
|
1935
|
+
// it neither breaks the no-op check below nor triggers a (futile,
|
|
1936
|
+
// on rootless) recreate when the override is cleared.
|
|
1937
|
+
const priorRequested = mergeResourceLimits(pluginDefault, currentOverrides[name]);
|
|
1938
|
+
const liveBefore = withoutInjectedResources(withoutDefaultMemorySwap(await getLiveResources(runtimeInfo, name), filteredLimits, await getRequestedResources(name)), filteredLimits, priorRequested);
|
|
1925
1939
|
// No-op when the live container already matches what's being
|
|
1926
1940
|
// requested. Verify existence by way of getLiveResources returning
|
|
1927
1941
|
// an empty object — if liveBefore is {}, either the container is
|
|
@@ -1946,14 +1960,6 @@ export default (app) => {
|
|
|
1946
1960
|
// update". Memory limits and oom-score-adj are the offenders —
|
|
1947
1961
|
// podman/docker can lower or raise them, but not return them to
|
|
1948
1962
|
// the unlimited/default state without a recreate.
|
|
1949
|
-
//
|
|
1950
|
-
// Provenance guard: only force a recreate for a field the consumer
|
|
1951
|
-
// actually requested before this update (plugin default ⊕ the
|
|
1952
|
-
// stored override). A field that shows up in `liveBefore` purely
|
|
1953
|
-
// because the runtime injected it (a rootless-Podman-inherited
|
|
1954
|
-
// oom_score_adj) was never the user's to unset, so clearing the
|
|
1955
|
-
// override must not trigger a (futile, on rootless) recreate.
|
|
1956
|
-
const priorRequested = mergeResourceLimits(pluginDefault, currentOverrides[name]);
|
|
1957
1963
|
const mustRecreateForUnset = fieldsRequiringRecreateForUnset(liveBefore, filteredLimits, priorRequested);
|
|
1958
1964
|
const forceRecreate = mustRecreateForUnset.length > 0;
|
|
1959
1965
|
if (forceRecreate) {
|
|
@@ -2012,10 +2018,33 @@ export default (app) => {
|
|
|
2012
2018
|
// target — it's what the consumer plugin most recently asked
|
|
2013
2019
|
// for, minus our new resources. (Bug A fix.)
|
|
2014
2020
|
try {
|
|
2015
|
-
await removeContainer(runtimeInfo, name
|
|
2021
|
+
await removeContainer(runtimeInfo, name, undefined, {
|
|
2022
|
+
graceful: true,
|
|
2023
|
+
});
|
|
2016
2024
|
}
|
|
2017
2025
|
catch (err) {
|
|
2018
|
-
|
|
2026
|
+
const removeMsg = err instanceof Error ? err.message : String(err);
|
|
2027
|
+
// A container that is still there keeps its old limits: resources
|
|
2028
|
+
// are not a drift axis, so the ensureRunning below would leave it
|
|
2029
|
+
// as it is and this would report a recreate that never happened.
|
|
2030
|
+
// When the stop went through and only the remove failed, it is
|
|
2031
|
+
// started again so a failed update does not leave it down.
|
|
2032
|
+
const state = await getContainerState(runtimeInfo, name);
|
|
2033
|
+
if (state !== "missing") {
|
|
2034
|
+
let restartNote = "";
|
|
2035
|
+
if (state === "stopped") {
|
|
2036
|
+
try {
|
|
2037
|
+
await startContainer(runtimeInfo, name);
|
|
2038
|
+
}
|
|
2039
|
+
catch (startErr) {
|
|
2040
|
+
restartNote = ` Starting it again also failed: ${startErr instanceof Error ? startErr.message : String(startErr)}`;
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
throw new Error(`Failed to apply new resources for ${name}: ${removeMsg}. ` +
|
|
2044
|
+
`The container was not removed and keeps its previous limits.` +
|
|
2045
|
+
restartNote, { cause: err });
|
|
2046
|
+
}
|
|
2047
|
+
warnings.push(`remove during recreate: ${removeMsg}`);
|
|
2019
2048
|
}
|
|
2020
2049
|
// A resource-update recreate re-runs ensureRunning, which re-fires
|
|
2021
2050
|
// device-passthrough events (a device may have been unplugged since
|