import type { ContainerResourceLimits, ContainerRuntimeInfo } from "./types.js"; import { type ContainerClient } from "./client.js"; /** * dockerode `HostConfig` resource fields. The Docker API takes CPU as a * NanoCpus integer and memory as a byte count, where the CLI took * `--cpus 1.5` and `--memory 512m`. `prefixedName` callers build this * fragment and merge it into a createContainer payload or pass it to * `container.update()`. */ export interface ResourceHostConfig { NanoCpus?: number; /** * CFS quota/period form of the CPU cap, used by the live-UPDATE path. * Podman's docker-compat `/update` endpoint does not apply `NanoCpus` * (the call succeeds but the cgroup cpu.max is left unchanged); it does * honour `CpuQuota`/`CpuPeriod`, which Docker accepts too. Create uses * `NanoCpus`; update uses these. `cpus` → quota = round(cpus*period), * period = 100000 (100ms, the Docker/podman default). */ CpuQuota?: number; CpuPeriod?: number; CpuShares?: number; CpusetCpus?: string; Memory?: number; MemorySwap?: number; MemoryReservation?: number; PidsLimit?: number; OomScoreAdj?: number; } /** * Parse a memory string the consumer-plugin / config-panel form uses * (`"512m"`, `"2g"`, `"1024k"`, `"536870912"`/`"...b"`) into bytes for * the Docker API. Uses binary units (1m = 1024*1024), matching how * `bytesToString` in containers.ts renders the round-trip. * * Throws on a malformed non-empty value (e.g. `"512mb"`, `"abc"`). This * is a system boundary: silently omitting the field would remove the cap * on create and let `tryLiveUpdate` report success after applying * nothing — a typo must fail loudly, before the container is touched. */ export declare function parseMemoryToBytes(value: string): number; /** * Compare a snapshot of currently-applied resource limits to a * target, and return the list of fields that the target is asking * to UNSET (i.e. fields present in `current` but absent or null in * `target`) which cannot be live-unset. * * If this returns a non-empty list, the caller MUST take the * recreate path — live update will silently no-op the unset. * * `current` is what `getLiveResources()` returned (the actual cgroup * state). `target` is the post-merge intended state. * * `priorRequested` (optional) is what the consumer plugin / user * actually *requested* before this reconcile — the pre-merge intent, * not the live cgroup state. When supplied, a field only counts as a * recreate-needing-unset if it was present in `priorRequested`. This * rules out values the runtime *injected* that the consumer never * asked for: rootless Podman clamps a child container's * `oom_score_adj` up to its parent's (it can only raise, never * lower), so a managed container started under a signalk-server whose * own `oom_score_adj` is non-zero (universal-installer Quadlet sets * `OOMScoreAdjust`, a hardened systemd user service, SK-in-a-container) * shows a non-zero `oomScoreAdj` in `current` that no plugin ever * requested. Without provenance, the diff misreads that artifact as * "user wants to unset a limit" and warns on every ensureRunning. * Omit `priorRequested` to keep the original current-vs-target * behaviour. */ export declare function fieldsRequiringRecreateForUnset(current: ContainerResourceLimits, target: ContainerResourceLimits, priorRequested?: ContainerResourceLimits): Array; /** * Compute the minimal override: the subset of `limits` fields whose * values actually differ from the consumer plugin's default. * * The config panel's resource editor seeds its form from the current * effective state (plugin default + override applied), which means * users who only want to change ONE field end up submitting a payload * containing all visible fields. Without minimization, every such * submission would store a full snapshot as the override, which: * 1. Shows "Override active" even when the submission matches * the plugin default exactly * 2. Pins the user to the old default values if the plugin author * bumps the default in a future version (e.g. if mayara bumps * memory from "512m" to "1g", a snapshot override from before * the bump would silently keep the user on "512m") * * This helper compares each field in `limits` against `pluginDefault` * and returns only the fields that represent a real user intent * different from the default. Rules: * * - `limits[k] === undefined` → drop (field not in submission) * - `limits[k] === null`: * - `pluginDefault[k] === undefined` → drop (both unset, no diff) * - `pluginDefault[k]` is set → KEEP (user explicitly unsetting * a value the plugin set — real intent) * - `limits[k]` has a value: * - equal to `pluginDefault[k]` → drop (redundant) * - different → KEEP (real user override) * * An empty result `{}` means "no override" — caller should delete the * container's entry from `currentOverrides` rather than store `{}`. * * Equality comparison is `===` for primitives. The only complex field * is cpusetCpus (a string); `1.5 === 1.5` for cpus works correctly. */ export declare function minimizeOverride(limits: ContainerResourceLimits, pluginDefault: ContainerResourceLimits): ContainerResourceLimits; export interface FilterResult { /** Limits with unsupported fields removed. */ accepted: ContainerResourceLimits; /** * Fields that were dropped, with the reason. Caller should log * these once (not on every reconcile) so the user knows their * override is being silently ignored. */ dropped: Array<{ field: keyof ContainerResourceLimits; reason: string; }>; } /** * Drop ContainerResourceLimits fields whose backing cgroup controller * is not available on this runtime. Returns the filtered limits and * a list of dropped fields for logging. * * Behavior: * - If `runtime.cgroupControllers` is `null` or `undefined`, all * fields are accepted (we have no information to filter against — * this is the default for docker, where we don't probe). * - If a field is `null` or `undefined` in the input, it's preserved * (the merge layer handles null=unset semantics). * - If a field's controller is in the available list, accepted. * - If a field's controller is missing, dropped with a clear reason. */ export declare function filterUnsupportedLimits(limits: ContainerResourceLimits, runtime: ContainerRuntimeInfo): FilterResult; /** * Field-level merge of two ContainerResourceLimits objects. The * override "wins" — but `null` in the override means "explicitly * remove the limit set by the base", whereas `undefined` means * "inherit from base". This matches RFC 7396 JSON merge patch * semantics for individual fields. * * Examples: * merge({ cpus: 1.5, memory: "512m" }, { cpus: 2.0 }) * → { cpus: 2.0, memory: "512m" } * merge({ cpus: 1.5, memory: "512m" }, { memory: null }) * → { cpus: 1.5 } * merge({ cpus: 1.5 }, undefined) * → { cpus: 1.5 } */ export declare function mergeResourceLimits(base: ContainerResourceLimits | undefined, override: ContainerResourceLimits | undefined): ContainerResourceLimits; /** * Translate ContainerResourceLimits into a dockerode `HostConfig` * resource fragment for `createContainer`. CPU becomes NanoCpus, memory * strings become byte counts. * * Fields whose backing cgroup controller is unavailable on the host are * silently dropped — caller should call `filterUnsupportedLimits` * separately if it wants to log the dropped set. */ export declare function resourcePayloadForRun(limits: ContainerResourceLimits | undefined, runtime: ContainerRuntimeInfo): ResourceHostConfig; /** * Translate ContainerResourceLimits into the flags accepted by * `podman update` / `docker update`. Returns null if the limits * contain a field that cannot be live-updated (e.g. cpuset on some * kernels, or oomScoreAdj which is set at create time only). * * Caller should fall back to recreate when this returns null. */ export declare function resourcePayloadForUpdate(limits: ContainerResourceLimits): ResourceHostConfig | null; /** * Attempt a live resource update on the named * container. Returns ok=true on success, ok=false on any failure * (caller is expected to fall back to recreate). * * The container name should already be the prefixed form (sk-...). * `exec` defaults to the production execRuntime; tests pass a stub. * * Behavior: * - Filters `limits` against `runtime.cgroupControllers` first. * Fields whose backing controller is unavailable are dropped * silently (caller logs them via filterUnsupportedLimits if it * wants visibility). * - If the filtered limits contain a field that cannot be * live-updated (e.g. cpuset, oomScoreAdj), returns ok=false so * the caller falls back to recreate. * - If the filtered limits are empty (every field was dropped), * verifies the container exists before claiming vacuous success. * This prevents Bug C: silent success when the container has * been removed out from under us. */ export declare function tryLiveUpdate(runtime: ContainerRuntimeInfo, fullName: string, limits: ContainerResourceLimits, client?: ContainerClient): Promise<{ ok: boolean; stderr?: string; }>; /** * Compare two ContainerResourceLimits for semantic equality (after * cleaning out null/undefined). Used to skip no-op updates when the * user saves the config panel without actually changing anything. */ export declare function resourceLimitsEqual(a: ContainerResourceLimits | undefined, b: ContainerResourceLimits | undefined): boolean; //# sourceMappingURL=resources.d.ts.map