import type Docker from "dockerode"; import { ContainerConfig, ContainerInfo, ContainerRuntimeInfo, ContainerState, EnsureRunningOptions, LocalImageSummary, ManagedImageRef, PruneResult, UlimitClamp, VolumeIssue, VolumeSpec } from "./types.js"; import { StreamingProcessHandle } from "./runtime.js"; import { type ContainerClient } from "./client.js"; import type { ErrorKind } from "./errors.js"; export declare function prefixedName(name: string): string; /** * Build the value for a `-v :[:flags]` argument with the * correct SELinux relabel suffix for the runtime. * * `:Z` is for SELinux relabelling of bind-mount host paths under Podman * on Fedora/RHEL. Named volumes (no leading '/' or '.') reject `:Z` with * "invalid option z for named volume", so we omit the flag for them. * * Used by both ContainerConfig.volumes (containers.ts) and JobConfig * inputs/outputs (jobs.ts) so the named-volume guard stays in one place. */ export declare function volumeArg(hostPath: string, containerPath: string, runtime: ContainerRuntimeInfo, readOnly?: boolean): string; /** * Classify each volume entry against host-source existence and the * declared `ifMissing` policy. Returns: * * - `kept`: the volumes that should be passed to the runtime, * normalized to bare-string form (the shape `buildRunArgs` and * `diffContainerConfig` already consume). * - `skipped`: host-path volumes whose source is missing AND whose * policy is `'skip'`. Caller should emit `onVolumeIssue` * events for each. * - `aborted`: host-path volumes whose source is missing AND whose * policy is `'abort'`. Caller should emit `onVolumeIssue` * events then throw. * * Bare-string entries and `ifMissing: 'create'` entries always end * up in `kept` (the runtime auto-creates the host dir on demand, * matching today's behaviour for the bare-string form). * * Named volumes (source without a leading `/` or `.`) always end up * in `kept` regardless of policy — the runtime owns their lifecycle. * * `probe` is the host-path existence check; defaults to `existsSync`. * Tests inject a stub so they don't touch the filesystem. */ /** * Default the in-container `HOME` to the bind-mounted config-root path * when the consumer plugin didn't set HOME themselves. CLI tools inside * the container (kopia, rclone, anything that reads ~/.cache or * ~/.config) need a writable home directory; the image's baked default * (typically /root or /app) is not writable when docker/rootful-podman * starts the container as the host caller's UID. Rootless podman * survives an unwritable HOME because the userns remap aliases /root to * the host caller, but setting HOME there too is harmless and keeps the * shape uniform across runtimes. * * Returns the env map the runtime should see — either the input * unchanged (HOME present, or no config root mount) or a new object * with HOME added. Pure and synchronous so it's testable in isolation. */ export declare function defaultHomeForConfigRoot(env: Record | undefined, configRootMount: string | undefined): Record | undefined; export declare function classifyVolumeSources(volumes: Record | undefined, probe?: (path: string) => boolean): { kept: Record; skipped: Array<{ containerPath: string; source: string; }>; aborted: Array<{ containerPath: string; source: string; }>; }; /** * Given the volume issues from the last `ensureRunning` call (both * skipped and aborted entries) and the current call's classification, * return the list of entries that are now present and applied — i.e. * recovered. Used to fire `onVolumeIssue` with `action: 'recovered'` * after the inner `ensureRunning` has recreated the container to * include the recovered mount. * * A volume has "recovered" when: * - it was in `prior.skipped` or `prior.aborted` (i.e. missing on * the last call), AND * - it is NOT in the current call's `currentSkipped` or * `currentAborted` (i.e. it is no longer missing), AND * - its `containerPath` is in `kept` (i.e. the runtime will actually * mount it this time). * * Pure function — no I/O. */ export declare function collectRecoveredVolumes(prior: { skipped: Array<{ containerPath: string; source: string; }>; aborted: Array<{ containerPath: string; source: string; }>; } | undefined, currentSkipped: Array<{ containerPath: string; source: string; }>, currentAborted: Array<{ containerPath: string; source: string; }>, kept: Record): Array<{ containerPath: string; source: string; }>; /** * Invoke an `onVolumeIssue` callback safely. Synchronous throws AND * rejected promises both route to `reportError`, so handler bugs (in * either flavour) never escape as unhandled rejections. * * The declared callback type is `(event) => void | Promise`, * but TS allows assigning a plain async function where `void` is * expected — the eventual rejection bypasses a naive `try/catch`. * Wrap the call in `Promise.resolve(...).catch(...)` so the same * error path catches both shapes. * * Pure-by-design: `reportError` is injected so tests can capture * the message instead of writing to `app.error`. */ export declare function safeInvokeVolumeIssue(handler: ((event: VolumeIssue) => void | Promise) | undefined, event: VolumeIssue, reportError: (err: unknown) => void): void; /** * Invoke an `onContainerLog` callback safely. Synchronous throws AND * rejected promises both route to `reportError`, so handler bugs (in * either flavour) never escape as unhandled rejections. Same shape * and rationale as `safeInvokeVolumeIssue` — see that helper's * doc for why the `try { Promise.resolve(...).catch(...) }` dance * is needed. */ export declare function safeInvokeContainerLog(handler: ((line: string) => void | Promise) | undefined, line: string, reportError: (err: unknown) => void): void; /** * Invoke an `onUlimitClamped` callback safely. Same shape and rationale * as `safeInvokeVolumeIssue` — see that helper's doc. */ export declare function safeInvokeUlimitClamped(handler: ((event: UlimitClamp) => void | Promise) | undefined, event: UlimitClamp, reportError: (err: unknown) => void): void; /** * Spawn `podman logs -f --tail=N sk-` (or `docker logs -f`) * and emit each line to `onLine`. Returns a stop-handle; the caller * manages lifecycle (start after the container is running; stop on * remove or recreate). * * `startTail` controls history-backfill on attach. Default 0 → * only live lines after attach. Set to e.g. 100 for "last 100 + * live" semantics. Both runtime CLIs accept `--tail` identically. * * `client` is exposed for tests (defaults to the dockerode singleton). */ export declare function tailContainerLogs(runtime: ContainerRuntimeInfo, name: string, onLine: (line: string) => void, options?: { startTail?: number; onError?: (msg: string) => void; onExit?: (code: number | null) => void; client?: ContainerClient; }): StreamingProcessHandle; /** Upper bound for `--tail N` accepted at the public boundary. */ export declare const MAX_TAIL = 10000; /** * Validate an optional unsigned-integer query parameter (e.g. * `?tail=200`, `?since=1700000000`) at the public REST boundary. * Returns `{ value }` on success (or `undefined` when the input * was omitted/empty) and `{ error }` with a human-readable * message for non-integer, negative, or non-finite inputs. * * Used by the `/logs` route to reject malformed inputs with 400 * instead of forwarding silently-coerced values to runtime-facing * logic. Exported so the parser is testable in isolation. */ export declare function parsePositiveIntQuery(raw: unknown, field: string): { value: number | undefined; error?: string; }; /** * Capture the last `tail` lines of a managed container's stdout * and stderr via `podman logs --tail ` (no `-f`). Returns the * array of lines. Caps `tail` at 10000 to prevent runaway-buffer * requests against very chatty containers; `since` is a unix-epoch- * seconds filter passed through to the runtime. * * Ordering caveat: the demuxed stream gives us stdout and stderr as * separate sinks; the OS-level chronological interleave between the two * is collapsed when we concatenate. We return the combined text split * into lines — a stderr line emitted between two stdout lines may land * out of order. Reconstructing true chronology would need per-line * `--timestamps` parsing and is out of scope. * * Used both by the `GET /containers/:name/logs` REST route and by * `ContainerManagerApi.getLogs` for in-process consumer-plugin calls. */ export declare function getContainerLogs(runtime: ContainerRuntimeInfo, name: string, options?: { tail?: number; since?: number; }, client?: ContainerClient): Promise; export declare function qualifyImage(image: string, runtime: ContainerRuntimeInfo): string; /** * The qualified repo forms a managed image can appear under in podman's * local `repoTags`. Usually one (the `qualifyImage` result), but a bare * single-name Docker Hub image like `alpine` qualifies to * `docker.io/alpine` while podman stores it as `docker.io/library/alpine` * — so both are returned, letting the reaper match either spelling. */ export declare function qualifiedRepoVariants(image: string, runtime: ContainerRuntimeInfo): string[]; export declare function imageExists(runtime: ContainerRuntimeInfo, image: string, client?: ContainerClient): Promise; /** * Return the local image ID (sha256 digest) for a given image reference, * or null if the image is not present locally. Used for digest-drift * detection of floating tags like :latest or :main. * * Pass either a repo:tag (e.g. "questdb/questdb:latest") to inspect a * pulled image, or a container name to inspect the image a running * container is using. */ export declare function getImageDigest(runtime: ContainerRuntimeInfo, imageOrContainer: string, client?: ContainerClient): Promise; /** * A Dockerfile `HEALTHCHECK` read back from an image's config. `test` is the * raw `Test` array (`["CMD-SHELL", "..."]` or `["CMD", "arg", ...]`); the * duration fields are nanoseconds (the runtime-agnostic JSON form). */ export interface ImageHealthcheck { test: string[]; intervalNs?: number; timeoutNs?: number; startPeriodNs?: number; retries?: number; } /** * Read an image's declared `HEALTHCHECK`, or `null` when it has none (or * declares `NONE`). We re-emit this as explicit `--health-*` flags at * `run` time (see `buildRunArgs`) because image-healthcheck inheritance is * not reliable across runtimes and API surfaces — notably it is dropped when * a container is created through Podman's Docker-compat socket, leaving the * container with an empty healthcheck and a perpetual `starting` state. * * Reads the config as JSON so duration fields come back as nanosecond * integers uniformly on podman and docker (a Go-template render diverges: * podman prints `30s`, docker prints the nanosecond count). */ export declare function getImageHealthcheck(runtime: ContainerRuntimeInfo, imageRef: string, client?: ContainerClient): Promise; /** * Return the first RepoDigest for an image reference, as * `sha256:` (without the `image@` prefix). This is the * *manifest* digest — what consumer plugins and CI tools speak — * distinct from the local image ID returned by `getImageDigest`. * * Returns null when the image has no RepoDigests, which happens for: * - locally-built images never pushed to a registry * - images side-loaded via `podman load` from a tarball * * The resolver falls back to a `local:` synthetic identity * in that case so the manifest entry still round-trips. */ export declare function getRepoDigest(runtime: ContainerRuntimeInfo, image: string, client?: ContainerClient): Promise; /** * Return the digest of the image a *running* container is actually * using, in the form `sha256:` or `local:`. * * Distinct from "what `image:tag` resolves to right now": if someone * `podman pull`'d the image after the container started, the local * tag moved but the container is still on the old bits. This walks * from the container's image-id (immutable for its lifetime) to its * RepoDigests. * * Returns null if the container doesn't exist or the runtime can't * read its image-id. */ export declare function getLiveContainerDigest(runtime: ContainerRuntimeInfo, containerName: string, client?: ContainerClient): Promise; /** * Pull an image, streaming progress to `onProgress`. dockerode's `pull` * returns a stream of JSON progress objects that must be followed to * completion via `modem.followProgress`; each event's `status`/`progress` * is surfaced line-wise to mirror the old CLI progress output. */ export declare function pullImage(runtime: ContainerRuntimeInfo, image: string, onProgress?: (msg: string) => void, client?: ContainerClient): Promise; export declare function getContainerState(runtime: ContainerRuntimeInfo, name: string, client?: ContainerClient): Promise; /** * Read the live resource limits applied to a managed container, * straight from `podman inspect` (i.e. the actual cgroup state). * Returns an empty object if the container is missing or no * limits are applied. Used by: * * - the `updateResources` rollback path, to capture pre-update * state so a failed recreate can be reverted * - `ensureRunning`'s diff detection, to decide whether a running * container needs a live resources update * * The shape conversion is the inverse of `resourceFlagsForRun`: * NanoCpus (nanoseconds/sec) → cpus (cores) * Memory (bytes) → memory ("123m") * MemorySwap (bytes) → memorySwap ("123m") * ...etc. * * Memory values are emitted as bytes-with-suffix to round-trip * cleanly with what consumer plugins typically pass in (`"512m"`). * * `exec` defaults to the production execRuntime; tests pass a stub. */ export declare function getLiveResources(runtime: ContainerRuntimeInfo, name: string, client?: ContainerClient): Promise; /** * One host-side endpoint for a single container port. Mirror of the * `{ HostIp, HostPort }` shape that podman/docker emit under * `NetworkSettings.Ports['/tcp']`. */ export interface PortBinding { hostIp: string; hostPort: number; } /** * Parse the JSON returned by `docker/podman inspect --format '{{json .NetworkSettings.Ports}}'` * into a Map keyed by integer container port. Pure function — used by both * `getActualPortBindings()` (against a live runtime) and the regression tests * (against synthetic JSON), so the parsing logic is covered without needing a * real container. * * Both runtimes accept multiple bindings per container port (one per host IP); * we keep all of them. UDP and SCTP entries are ignored — only `/tcp` is * relevant for `signalkAccessiblePorts`. * * Returns an empty map on null/empty input or when JSON is malformed; callers * already handle "no binding found" so a parse error degrades gracefully into * "leave the existing cache alone". */ export declare function parsePortBindings(json: string | Record | null | undefined): Map; /** * Read the live host-side port bindings for a managed container straight from * the runtime, bypassing any in-process cache. Returns a Map keyed by the * container's internal TCP port. * * Used by `ensureRunning` to validate `pendingPortMap` against reality just * after the runtime has bound the ports — closes the TOCTOU window between * `findAvailablePort()`'s in-process probe and `podman create`'s actual bind. * * Returns an empty map on inspect failure; callers fall back to whatever they * had pre-validation. */ export declare function getActualPortBindings(runtime: ContainerRuntimeInfo, name: string, client?: ContainerClient): Promise>; /** * The recreate-requiring half of a live container's effective config, parsed * from a single `inspect` call. Mirror of the recreate-requiring fields on * `ContainerConfig` after `buildRunArgs` would have rendered them. Resources * have their own `getLiveResources` reader and live-update path. * * `image+tag` come from `.Config.Image` (the as-passed reference like * `questdb/questdb:latest`), not `.Image` (the resolved sha256 digest) — * digest-drift detection is the update service's job (`src/updates/`). */ export interface LiveContainerConfig { image: string; tag: string; /** * Set when the live `Config.Image` was a digest-pinned reference * (`image@sha256:...`). `tag` then holds whatever the runtime * reports alongside (typically the empty string or `"latest"`); the * authoritative comparison should use `digest`. */ digest: string | null; command: string[] | null; networkMode: string; env: Map; binds: Array<{ host: string; container: string; }>; portBindings: Map; extraHosts: Map; /** * Effective `--user` spec from `.Config.User`. Empty string when the * container was created without `--user` (image USER, typically root). * Drift detection compares this against the expected mapping derived * from the requested `ContainerConfig.user` and the runtime's host * UID/GID. * * Note: `--userns=keep-id` doesn't surface in `.Config.User`; recreating * on `user` drift handles the practical case (Podman picks up the * keep-id flag on the new run). */ user: string; } /** * Read the live equivalent of `ContainerConfig`'s recreate-requiring fields * for an already-running container. Returns `null` on inspect failure (the * caller treats that as "can't diff, fall back to early-return" — fail-safe). * * Used by `ensureRunning` to detect drift between the requested config and * what the container was actually created with, so a recreate can fire * automatically instead of silently ignoring the new config until restart. */ export declare function getLiveContainerConfig(runtime: ContainerRuntimeInfo, name: string, client?: ContainerClient): Promise; /** * Compare a requested `ContainerConfig` against the live container's * effective config and return the list of fields that have drifted. * * Pure function — no I/O. The caller (`ensureRunning`) decides what to do * with a non-empty drift list (today: log + remove + recreate). * * Field semantics: * - image+tag: tag-string equality only, never digest. Update detection * for floating tags (`:latest` digest drift) is the update service's job. * - command: explicit drift when `requested.command` is set and differs * from `live.command`. When `requested.command` is undefined, drift is * reported only if a `prior.command` was set (i.e. the user is now * unsetting it). Without a `prior`, an undefined `requested.command` * can't be told apart from "image's baked CMD" so we skip — the * wrapper's prior-config cache (`lastConfigs`) closes that loop on the * second-and-later calls within a single signalk-container lifetime. * - networkMode: runtime defaults (`bridge`, `slirp4netns`, etc.) are * normalized to `""` and compared as equivalent to requested undefined. * - env: requested keys must match live values. Additionally, any key * present in `prior.env` but absent from `requested.env` is treated * as drift (the user is unsetting it). Image-baked env keys not in * either `requested.env` or `prior.env` are ignored — they were never * ours. * - volumes: trailing slashes stripped on both sides; `(host, container)` * tuples compared as a Map keyed by container path. Live binds have * their `:Z`/`:ro` flags already stripped by `getLiveContainerConfig`. * - ports: container-port key compared as runtime-emitted (`9000/tcp`). * Multiple host bindings per container port compared as a sorted set. */ export declare function diffContainerConfig(requested: ContainerConfig, live: LiveContainerConfig, runtime: ContainerRuntimeInfo, prior?: ContainerConfig): { drifted: string[]; }; /** * Read the highest `nofile` (`RLIMIT_NOFILE`) hard limit a container on * this host can actually be given, so an over-request can be clamped * instead of being rejected at container-create time. * * The ceiling differs by privilege: * - rootless: a container cannot raise its hard limit above the * calling user's hard limit — only privileged processes may. Signal * K server runs as that same user, so its own hard limit * (`/proc/self/limits`) IS the ceiling. crun rejects anything higher * with `setrlimit RLIMIT_NOFILE: Operation not permitted` and the * container fails to start. * - rootful / docker: the runtime is privileged and can raise up to * the kernel's absolute per-process cap, `fs.nr_open`. * * Returns `null` when the ceiling can't be determined (non-Linux, the * proc files are absent) — the caller then passes the request through * unclamped, preserving today's behaviour on those platforms. */ export declare function readNofileHardCeiling(runtime: ContainerRuntimeInfo): number | null; /** * Translate `ContainerConfig.ulimits` into the dockerode * `HostConfig.Ulimits` array (`{ Name, Soft, Hard }`). A bare number sets * soft = hard. Returns `undefined` when no ulimits are configured so the * caller can leave the field unset. * * `nofileCeiling`, when set, caps both the soft and hard `nofile` limits * to a value the host can actually grant — a rootless container that * requests more `nofile` than the calling user's hard limit is rejected * by crun and fails to start, so clamping turns a fatal over-request into * the best limit the host can deliver. `onClamp` fires once when the * `nofile` request is lowered, so the caller can log an advisory. * * Throws on an invalid limit (non-integer, negative, or `hard < soft`) so * a bad consumer config fails early with a descriptive message rather than * as an opaque runtime create error. Exported for unit testing. */ export declare function ulimitsForRun(ulimits: ContainerConfig["ulimits"], nofileCeiling?: number | null, onClamp?: (requested: number, granted: number) => void): Docker.Ulimit[] | undefined; /** * Pull function injected into `ensureRunning` for testability. Production * uses the module-level `pullImage` (which pulls via the dockerode client). * Tests pass a stub to assert call counts and simulate offline failures * without touching the network. */ type PullFn = (runtime: ContainerRuntimeInfo, image: string, onProgress?: (msg: string) => void) => Promise; export declare function ensureRunning(runtime: ContainerRuntimeInfo, name: string, config: ContainerConfig, debug: (msg: string) => void, options?: EnsureRunningOptions, client?: ContainerClient, /** * Prior `ContainerConfig` from the previous `ensureRunning` call within * this signalk-container lifetime, if any. Used to detect "unset" drift * — env keys removed, `command` previously set and now undefined. The * wrapper in `index.ts` reads from its `lastConfigs` cache before * overwriting it; on the first call (or after a Signal K restart) this * will be undefined and only positive drift is detected. */ prior?: ContainerConfig, _postRecreate?: boolean, _pull?: PullFn): Promise; export declare function startContainer(runtime: ContainerRuntimeInfo, name: string, client?: ContainerClient): Promise; export declare function stopContainer(runtime: ContainerRuntimeInfo, name: string, client?: ContainerClient): Promise; export declare function removeContainer(runtime: ContainerRuntimeInfo, name: string, client?: ContainerClient): Promise; /** * Thrown by `removeContainer` when the runtime refuses to remove a container. * Carries the classified `kind` so callers can distinguish a `permission` * failure (a rootless container wedged unkillable in `Stopping`) — where * keeping a healthy existing container beats failing startup — from other * removal failures that should propagate. */ export declare class ContainerRemovalError extends Error { readonly kind: ErrorKind; constructor(message: string, kind: ErrorKind); } /** * Reject host paths that must never be recursively wiped. The guard is the * cheap, always-correct floor (empty / `/` / a non-absolute path); the * caller layers any deployment-specific "must be under the Signal K tree" * check on top, where it has the data/config roots to compare against. */ export declare function assertWipablePath(hostPath: string): void; /** * Result of one wipe-job run, mirroring the bits of `ContainerJobResult` * that `removeManagedData` reasons about. Keeps `containers.ts` free of a * `jobs.ts` import (jobs.ts already imports from here — the cycle would be * real) while staying fully testable via an injected runner. */ export interface WipeJobOutcome { ok: boolean; error?: string; } /** * Delete a managed container's bind-mount data, working around the * rootless-Podman subuid-ownership trap. * * Sequence: * 1. Remove the container `name` (idempotent — a missing container is fine) * so nothing holds the mount. * 2. Try a direct host-side `fs.rm(hostPath, {recursive, force})`. On * docker / rootful Podman the files are host-owned and this succeeds. * 3. On EACCES/EPERM (the rootless-Podman case — files are owned by a * subuid the host user can't touch) run `runWipeJob`: a one-shot helper * that bind-mounts `hostPath` and `rm -rf`s its CONTENTS from inside the * userns as in-container root, then retry the host-side delete to drop * the now-empty host-owned parent dir. * * `runWipeJob(image, hostPath)` is injected so the runtime logic stays out * of the `jobs.ts` import cycle and so unit tests can drive every branch * without a real runtime. The wrapper in `index.ts` wires it to `runJob` * with the container's own (already-present) image. * * `wipeImage` is the container's own image, captured from `inspect` BEFORE * removal so the helper reuses bits already on disk — no registry pull on a * possibly-offline boat. `null` when the container was already gone; the * fallback then can't run, so a still-undeletable dir surfaces as an error. */ export declare function removeManagedData(runtime: ContainerRuntimeInfo, name: string, hostPath: string, runWipeJob: (image: string, hostPath: string) => Promise, client?: ContainerClient, onRemoved?: () => void): Promise; /** * Container path the wipe helper mounts the data directory at — re-exported * for the wrapper that builds the `runJob` config and for tests asserting the * mount/command shape. */ export declare const WIPE_MOUNT_PATH = "/sk-wipe-target"; export declare function listContainers(runtime: ContainerRuntimeInfo, client?: ContainerClient): Promise; export declare function pruneImages(runtime: ContainerRuntimeInfo, client?: ContainerClient): Promise<{ imagesRemoved: number; spaceReclaimed: string; }>; /** * Pure selection: given every local image, the managed repos with their * running image-IDs, and how many prior versions to keep, return the * de-duplicated image-IDs to remove. * * `managed[].image` must already be in the same qualified form the local * `repoTags` use (the executor qualifies via `qualifyImage` at the * boundary), so a bare Docker Hub repo and its `docker.io/`-prefixed * local tag compare equal. * * Guarantees, by construction: * - unrelated images are never returned (an image is only considered * under a repo it is actually tagged for, and only managed repos * are walked); * - the running image is never returned (excluded by image-ID); * - an image in use by any container (running OR stopped) is never * returned (excluded by `inUseCount`); * - the newest `keepImageVersions` superseded versions per repo survive; * - an image shared across managed repos survives if ANY repo retains * it (reaping it for one would untag it from the others). */ export declare function selectImagesToReap(images: LocalImageSummary[], managed: ManagedImageRef[], keepImageVersions: number): string[]; /** * Remove superseded versions of managed-container images, keeping the * running one plus `keepImageVersions` prior versions per repo. The * selection is delegated to the pure `selectImagesToReap`; this layer * only does I/O (list, remove) and never throws — a listing failure or a * single stuck image must not abort the rest of the scheduled tick. */ export declare function reapSupersededImages(runtime: ContainerRuntimeInfo, managed: ManagedImageRef[], keepImageVersions: number, client?: ContainerClient): Promise; export declare function execInContainer(runtime: ContainerRuntimeInfo, name: string, command: string[], client?: ContainerClient): Promise<{ exitCode: number; stdout: string; stderr: string; }>; export declare function ensureNetwork(runtime: ContainerRuntimeInfo, name: string, client?: ContainerClient): Promise; export declare function removeNetwork(runtime: ContainerRuntimeInfo, name: string, client?: ContainerClient): Promise; export declare function connectToNetwork(runtime: ContainerRuntimeInfo, containerName: string, networkName: string, client?: ContainerClient): Promise; export declare function disconnectFromNetwork(runtime: ContainerRuntimeInfo, containerName: string, networkName: string, client?: ContainerClient): Promise; /** * Extract a container ID from a `/proc/self/cgroup` line, if present. * Exposed for unit tests; production callers go through * `findSelfContainerId`. * * Handles the formats we see in the wild: * * cgroup v1 + Docker: * 12:cpuset:/docker/0123abc...def * * cgroup v2 + Docker on systemd: * 0::/system.slice/docker-0123abc...def.scope * * cgroup v2 + Podman rootless on systemd: * 0::/user.slice/user-1000.slice/.../libpod-0123abc...def.scope * * Kubernetes / containerd (best-effort): * 0::/kubepods.slice/.../cri-containerd-0123abc...def.scope * * Returns null when no recognisable container-id token is found — * callers fall through to the next cascade step. We accept any * 12+ hex char run; `docker inspect` will reject false positives * (e.g. systemd slice names that happen to be hex). */ export declare function parseSelfContainerIdFromCgroup(line: string): string | null; /** * Pure: extract every parseable container-ID candidate from a multi- * line cgroup file content, in source-line order, deduplicated. * Returns an empty array when no line yields a candidate. * * Exists separately from `readSelfContainerIdsFromCgroup` so unit * tests can drive the multi-line / dedup logic without touching * `/proc/self/cgroup` (which varies across test hosts). */ export declare function parseSelfContainerIdsFromCgroupFile(content: string): string[]; /** * Read `/proc/self/cgroup` (cgroup v1: many lines, one per controller, * all typically pointing at the same container; v2: single `0::/...` * line) and extract every recognisable container-ID candidate. * Returned in source-line order, deduplicated. Empty array when not * in a container, when the file isn't readable, or when no parseable * id is present. * * Returning all candidates instead of just the first lets * `findSelfContainerId` validate each via `inspect` and skip false * positives — important because `parseSelfContainerIdFromCgroup`'s * regex is permissive on purpose (matches short 12-char ids and * various runtime prefixes), so a non-container path that happens * to embed a 12+ hex run could otherwise short-circuit detection. * * Skipped in production paths when `isContainerized()` is false. */ export declare function readSelfContainerIdsFromCgroup(): string[]; /** * Pure: extract every parseable container-ID candidate from a * `/proc/self/mountinfo` style payload, in source-line order, * deduplicated. The runtime stamps the full container id into the * mount source path for the bindfs files it injects (`/etc/hostname`, * `/etc/resolv.conf`, `/run/.containerenv`): * * - Podman: `…/containers/overlay-containers/<64-hex>/userdata/…` * - Docker: `/<64-hex>/…` (rooted at the storage driver's container dir) * * Both patterns yield the same 64-character id when matched. We * require 64 hex chars (not 12+) to avoid matching arbitrary content- * addressed paths like overlay layers (which use the same hex length * but DIFFERENT ids). * * Exists separately from `readSelfContainerIdsFromMountinfo` so unit * tests can drive the parser without touching `/proc/self/mountinfo`. */ export declare function parseSelfContainerIdsFromMountinfo(content: string): string[]; /** * Read `/proc/self/mountinfo` and extract every recognisable container * id. Returned in source-line order, deduplicated. Empty array when * the file isn't readable or no id is present. * * This is the fourth detection step in `findSelfContainerId`, picking * up the case where: * - SIGNALK_CONTAINER_ID is unset, AND * - HOSTNAME is empty (Quadlet doesn't set it for the container * env by default), AND * - /proc/self/cgroup is `0::/` (split-cgroups Quadlet setup), AND * - the container is running with `Network=host` (so /etc/hostname * returns the host machine name, not the container name). * * The mountinfo source path is the same for every container the * runtime starts, so it works regardless of network mode, cgroup * delegation, or whether HOSTNAME is set. */ export declare function readSelfContainerIdsFromMountinfo(): string[]; /** * Find this signalk-server's own container id, cascading across the * known-reliable signals to the brittle ones: * * 1. `SIGNALK_CONTAINER_ID` env var — explicit override. Doc-ed * escape hatch for any deployment where automatic detection * proves unreliable. * 2. `HOSTNAME` env var — works in default-network deployments * where Docker/Podman set HOSTNAME to the (short) container id. * Validated via `inspect`: under `network_mode: host` the * container inherits the host's hostname (e.g. "halos") and * `inspect` fails — we fall through to the next step. This * is the bug fixed by this helper (issue #23). * 3. `/proc/self/cgroup` — robust against host-network mode and * any other case where HOSTNAME is wrong. The id we extract * gets the same `inspect`-validation treatment. * 4. `/proc/self/mountinfo` — the runtime stamps the full * container id into the source path of `/etc/hostname`, * `/etc/resolv.conf`, `/run/.containerenv` and friends. This * catches the case where step 2 and step 3 both fail: under a * Podman Quadlet with `Network=host`, HOSTNAME is empty (the * Quadlet doesn't forward it into the container's environment) * AND `/proc/self/cgroup` is the rootless `0::/` placeholder. * mountinfo is set by the runtime regardless of network mode or * cgroup delegation, so this step succeeds where the others * can't. * * Returns null when none of the cascade steps yield a valid id. * Callers should treat this exactly like the previous "HOSTNAME * unset" behaviour — fall back to bare-metal-style handling, or * return null to the caller (depends on the consumer). */ export declare function findSelfContainerId(runtime: ContainerRuntimeInfo, debug?: (msg: string) => void, client?: ContainerClient): Promise; /** * Resolve what to mount in a managed container to give it access to * the SignalK data directory, regardless of how SignalK itself is deployed. * * Returns the string to use as the LEFT side of a `-v :` flag: * - Bare-metal SignalK: returns dataDir directly (it is already a host path). * - SignalK in Docker, volume-backed dataDir: returns the named volume. * - SignalK in Docker, bind-backed dataDir: returns the exact host path * (computing the subpath when a parent directory is bind-mounted). * - Fallback (mount not found): returns dataDir — the caller's `-v` will * fail gracefully at container-create time with a clear Docker error. * * The result can be used directly as `volumes: { [mountPoint]: source }` in * a ContainerConfig. The content visible at mountPoint inside the managed * container will always correspond to the root of dataDir. */ export declare function resolveSignalkDataSource(dataDir: string, runtime: ContainerRuntimeInfo, debug?: (msg: string) => void, client?: ContainerClient): Promise; /** * Mount entry as parsed from `podman inspect --format '{{range .Mounts}}...'`. * Exposed so tests can drive `resolveHostPathFromMounts` directly without * touching a real runtime. */ export interface InspectedMount { type: string; name: string; source: string; dest: string; } /** * Pure (source, subPath) resolution given a list of mounts and an * absolute path. Factored out of `resolveHostPath` so the matching * logic can be unit-tested independently of the runtime. Returns null * when no mount covers `absPath`. */ export declare function resolveHostPathFromMounts(absPath: string, mounts: InspectedMount[]): ContainerMountResolution | null; /** * Result shape for `resolveHostPath`. * * `source` is the LEFT side of a `-v :` flag, suitable for * dropping into ContainerJobConfig.inputs / outputs as the value: * * runJob({ inputs: { "/in": resolution.source }, ... }) * * `subPath` is the path INSIDE the mount where the original absolute * path lives. Empty string when the source already corresponds to * the absolute path (no further indirection needed). Otherwise the * consumer must navigate to it from the mount root, e.g.: * * command: ["gdal_translate", `/in/${resolution.subPath}/file.000`, ...] * * The slash-prefix discipline is the consumer's: `subPath` itself never * has a leading slash. */ export interface ContainerMountResolution { source: string; subPath: string; } /** * Translate an arbitrary absolute path into the `(source, subPath)` pair * that lets a managed container reach that path on the host, regardless * of how SignalK itself is deployed. Generalises * `resolveSignalkDataSource` to paths outside `app.getDataDirPath()`. * * Use cases: * - SignalK in a container with a bind mount that covers a *parent* * directory (typical: `-v /opt/signalk:/home/node/.signalk`). * Both the data dir and any sibling chart directory under `/opt/signalk` * are reachable through the same mount. * - SignalK in a container with a named volume that covers a parent * directory. The same volume name is returned and the consumer * mounts it whole — the runtime cannot subpath-mount volumes. * - Bare-metal SignalK: any absolute path is its own host path. * * Returns `null` when: * - The runtime can't be inspected (we couldn't determine our own * mounts, e.g. HOSTNAME unset, inspect failed). * - We're inside a container but no mount covers `absPath` — meaning * the host runtime physically cannot see this path. The consumer * should surface an actionable error (not silently fall through, as * `resolveSignalkDataSource` does for backwards-compat). */ export declare function resolveHostPath(absPath: string, runtime: ContainerRuntimeInfo, debug?: (msg: string) => void, client?: ContainerClient): Promise; /** * Release a port that was reserved by `findAvailablePort()`. * Must be called after the container runtime has successfully bound the port * (so the OS-level bind now prevents collisions), or when the container * creation failed (so the next attempt can re-probe freely). */ export declare function releaseReservedPort(port: number): void; /** * Find the lowest available TCP port on 127.0.0.1 starting at `preferred`. * * Probes by briefly binding a server socket and also skips ports that are * already reserved in-process by a concurrent `findAvailablePort()` call, * eliminating the TOCTOU window between the probe and the container create. * * The chosen port is added to the process-local `reservedPorts` set before * this function resolves. The caller is responsible for releasing it via * `releaseReservedPort()` once the runtime holds the binding or the attempt * fails. * * Used by the `signalkAccessiblePorts` bare-metal path to prefer the * declared port number while gracefully stepping over conflicts. */ export declare function findAvailablePort(preferred: number): Promise; /** * From the network names a container is attached to, return only the ones that * support same-network container-name DNS — i.e. drop the reserved defaults * (`bridge`, `podman`, `host`, `none`). Pure so it can be unit-tested without a * runtime; `resolveSignalkNetworks` wraps it around live inspect data. */ export declare function userDefinedDnsNetworks(networkNames: string[]): string[]; /** * Return the user-defined Docker/Podman networks that the current SignalK * container is connected to (i.e. networks other than the default `bridge`, * `host`, or `none`). * * Used by the `signalkAccessiblePorts` containerized path to attach a * managed container to SignalK's own network so the two can communicate * via DNS name without exposing any host port. * * Returns: * - `null` when running bare-metal, or when self-container detection * fails (`SIGNALK_CONTAINER_ID` unset, HOSTNAME unusable * under `network_mode: host`, and `/proc/self/cgroup` not * parseable). Callers should treat this like bare-metal * and publish ports instead. * - `string[]` (possibly empty) when inspect succeeds. An empty array means * SignalK is only on the default bridge — callers should fall * back to `networkMode: container:`. A non-empty * array contains the user-defined network names to attach to. */ export declare function resolveSignalkNetworks(runtime: ContainerRuntimeInfo, debug?: (msg: string) => void, client?: ContainerClient): Promise; export declare function waitForReady(url: string, timeoutMs?: number, intervalMs?: number): Promise; export {}; //# sourceMappingURL=containers.d.ts.map